当前主流方案
常量
ini
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其实也是在定义一组常量,不过是将赋值的动作自动化了,并且限定了枚举的基础类型。
go
type Status int
const (
Created Status = iota
Pending
Success
Failed
)
iota这种方式只是方便了开发而已,实际相对与手工赋值更加危险,因为一旦枚举的顺序被打乱(插入、删除枚举),实际原来的枚举值就变了,反序列化会遇到坑。
而当我们想要获得枚举的字符串表示,或者要将一个字符串转化为枚举时,常量模式就必须再维护一个字符串数组与枚举一一对应,手工映射,如下。
go
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
go
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
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")
)
枚举使用
scss
// 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对多
下面是使用枚举框架,定义实现的模块、角色、权限模型
go
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...