likes
comments
collection
share

Golang枚举最佳实践

作者站长头像
站长
· 阅读数 9

当前主流方案

常量

type Status int
const (
    Created Status  = 1
    Pending Status  = 2
    Success Status  = 3
    Failed  Status  = 4
)

type Status string 

const (
    Created Status  = "Created"
    Pending Status  = "Pending"
    Success Status  = "Success"
    Failed  Status  = "Failed"
)

Iota

iota其实也是在定义一组常量,不过是将赋值的动作自动化了,并且限定了枚举的基础类型。

type Status int 

const (
    Created Status = iota
    Pending
    Success 
    Failed
)

iota这种方式只是方便了开发而已,实际相对与手工赋值更加危险,因为一旦枚举的顺序被打乱(插入、删除枚举),实际原来的枚举值就变了,反序列化会遇到坑

而当我们想要获得枚举的字符串表示,或者要将一个字符串转化为枚举时,常量模式就必须再维护一个字符串数组与枚举一一对应,手工映射,如下。

type Status int 

const (
    Created Status = iota
    Pending
    Success 
    Failed
)

var allStatus = []string{"Created","Pending","Success","Failed"}

func (s Status) Name() string {
    return allStatus[s]
}

func ValueOf(name string) (Status,err) {
    for e,s := range allStatus {
        if s == name {
            return Status(e),nil
        }
    }
    return Created,errors.New("enum not found")
}

这个模式的问题在于

  • 字符串数组必须与常量一一对应,不能出现遗漏或者乱序,依赖开发人工保证

  • 每定义一个枚举,可能都需要撸一遍Name和ValueOf方法。

常量模式还存在一个巨大的问题:不能很好的封装成员属性,因为基础类型就不是struct。就如枚举的字符串表示,其实可以看作需要给枚举封装一个name成员,此时必须在枚举之外维护成员,并确保映射关系,才能实现多个成员的复杂封装。

Struct

type Status struct {
    name string
    desc string
}

var enumMap = make(map[string]Status)

var enums []Status

func ValueOf(name string) (Status,error) {
    if e,ok := enumMap[name]; ok  {
        return e,nil
    }
    return Status{},errors.New("enum not found")
}

func Values() []Status {
    return enums
}

func NewStatus(name , desc string) Status {
    res := Status{name,desc}
    enumMap[name] = res
    enums = append(enums,res)
    return res
}

func (s Status) Name() string {
    return s.name
}

func (s Status) Desc() string {
    return s.desc
}



var (
    Created = NewStatus("Created","已创建")
    Pending = NewStatus("Pending","运行中")
    Success = NewStatus("Success","成功")
    Failed  = NewStatus("Failed","失败")
)

这种方案的优点显而易见,使用struct代表枚举类型,便可以在枚举中封装很多的成员属性。业务开发中实际存在许多的复杂枚举,比如errorcode、role/permissions等。

缺点也很突出,每定义一个枚举,都需要自己实现NewXxx、ValueOf、Values等方法

枚举的核心诉求

  • 定义简单

    • 应该用尽可能少的代码,实现枚举定义,并拥有开箱即用的一组实例方法和工具方法。
  • 不可变

    • 枚举应该仅在构造过程中可以修改,构造完之后,应该是一个只读对象。
  • 可枚举、迭代

    • 枚举的数量是有限的,并且有类似values方法,获取到某个类型的全部枚举,实现一些遍历逻辑
  • 支持格式化、序列化、反序列化

    • 枚举应该可以支持fmt等方法的格式化,也应该能支持json序列化和反序列化。业务开发常常存在将枚举序列化到DB、redis等情况。
  • 拥有序数,支持比较

    • 部分枚举,是符合单链递进特征的,典型的比如status,这类枚举很多时候是希望支持比较的。

终极解决方案

基于以上这些诉求(痛点),考虑到go在1.18之后已经支持了泛型,遂考虑使用泛型+反射实现一个通用枚举方案。最终实现效果如下:

只需往枚举struct内嵌goenum.Enum(组合), 即可定义一个枚举类型,并获得开箱即用的一组方法。

枚举定义

go get github.com/lvyahui8/goenum

import "github.com/lvyahui8/goenum"

// 声明枚举类型
type State struct {
    goenum.Enum
}

// 定义枚举
var (
    Created = goenum.NewEnum[State]("Created")
    Running = goenum.NewEnum[State]("Running")
    Success = goenum.NewEnum[State]("Success")
)

枚举使用

// Usage
Created.Name() // string "Created"
Created.Ordinal() // int 0
Running.Compare(Created) > 0 // Running > Created
Created.Equals(*goenum.ValueOf[State]("Created")) // true

*goenum.ValueOf[State]("Created") // struct instance: Created
goenum.Values[State]() // equals []State{Created,Running,Success}
goenum.IsValidEnum("Created") // true

更多特性

除此之外

  • 枚举默认实现了fmt.Stringer、json.Marshaler ,天然支持了格式化和序列化(反序列化可以使用ValueOf方法)。

  • 框架还开发了枚举初始化能力,枚举struct 实现Init值方法即可,NewEnum方法中的args参数,会完整透传给Init方法。注意,Init方法需要将receiver返回以确保初始化生效。Init导出也不存在安全问题,这限定为一个值方法,即使调用也不会被修改。

  • 框架基于位图实现了EnumSet,在枚举数量较多的时候,可以获得比map更稳定的性能。

示例:Gitlab/Github权限模型

Gitlab的权限模型,是枚举的典型使用场景之一,划分了复杂的模块、角色和权限。具体划分见:docs.gitlab.com/ee/user/per…

  • 模块->权限: 1对多

  • 角色->权限: 1对多

Golang枚举最佳实践

下面是使用枚举框架,定义实现的模块、角色、权限模型

package internal

import "github.com/lvyahui8/goenum"

func castList[T any](items ...any) (res []T) {
   for _, item := range items {
      if v, ok := item.(T); ok {
         res = append(res, v)
      }
   }
   return
}

// Role 参考 https://docs.gitlab.com/ee/user/permissions.html
type Role struct {
   goenum.Enum
   perms []Permission
}

func (r Role) Init(args ...any) any {
   r.perms = castList[Permission](args...)
   return r
}

func (r Role) HasPerm(p Permission) bool {
   for _, perm := range r.perms {
      if p.Equals(perm) {
         return true
      }
   }
   return false
}

type Module struct {
   goenum.Enum
   perms    []Permission
   basePath string
}

func (m Module) Init(args ...any) any {
   m.perms = args[0].([]Permission)
   m.basePath = args[1].(string)
   return m
}

func (m Module) GetPerms() []Permission {
   return m.perms
}

func (m Module) BasePath() string {
   return m.basePath
}

type Permission struct {
   goenum.Enum
}

// 定义权限
var (
   AddLabels           = goenum.NewEnum[Permission]("AddLabels")
   AddTopic            = goenum.NewEnum[Permission]("AddTopic")
   ViewMergeRequest    = goenum.NewEnum[Permission]("ViewMergeRequest")
   ApproveMergeRequest = goenum.NewEnum[Permission]("ApproveMergeRequest")
   DeleteMergeRequest  = goenum.NewEnum[Permission]("DeleteMergeRequest")
)

// 定义模块
var (
   Issues        = goenum.NewEnum[Module]("Issues", []Permission{AddLabels, AddTopic}, "/issues/")
   MergeRequests = goenum.NewEnum[Module]("MergeRequests", []Permission{ViewMergeRequest, ApproveMergeRequest, DeleteMergeRequest}, "/merge/")
)

// 定义角色
var (
   Reporter  = goenum.NewEnum[Role]("Reporter", ViewMergeRequest)
   Developer = goenum.NewEnum[Role]("Developer", AddLabels, AddTopic, ViewMergeRequest)
   Owner     = goenum.NewEnum[Role]("Owner", AddLabels, AddTopic, ViewMergeRequest, ApproveMergeRequest, DeleteMergeRequest) // 可以考虑给Owner单独定义一个All的权限
)

项目地址

欢迎使用&提Issues。github.com/lvyahui8/go…