Golang枚举最佳实践

当前主流方案

常量

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...

相关推荐
我的golang之路果然有问题4 分钟前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
Gladiator5759 分钟前
博客记录-day152-力扣+分布式
github
jstart千语13 分钟前
【Git】连接github时的疑难杂症(DNS解析失败)
git·github
柏油13 分钟前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql
写bug写bug2 小时前
Java Streams 中的7个常见错误
java·后端
Luck小吕2 小时前
两天两夜!这个 GB28181 的坑让我差点卸载 VSCode
后端·网络协议
M1A12 小时前
全栈开发必备:Windows安装VS Code全流程
前端·后端·全栈
蜗牛快跑1232 小时前
github 源码阅读神器 deepwiki,自动生成源码架构图和知识库
前端·后端
嘻嘻嘻嘻嘻嘻ys2 小时前
《Vue 3.4响应式超级工厂:Script Setup工程化实战与性能跃迁》
前端·后端
橘猫云计算机设计2 小时前
net+MySQL中小民营企业安全生产管理系统(源码+lw+部署文档+讲解),源码可白嫖!
数据库·后端·爬虫·python·mysql·django·毕业设计