Go 语言中 enum 实现方式有哪些?一定要绝对类型安全吗?
2024-02-02 17:01:25 软件 223观看
摘要嗨!大家好,本文 Go 语言小技巧系列的第十二篇,往期文章查看:Go 语言小技巧。你是否了解过 Go 中的枚举呢?枚举,即 enum,可用于表示一组范围固定的值,它能助我们写出清晰、安全的代码。以编写游戏程序为一个简单案例:游戏中的角

嗨!大家好,本文 Go 语言小技巧系列的第十二篇,往期文章查看:Go 语言小技巧。Wqw28资讯网——每日最新资讯28at.com

你是否了解过 Go 中的枚举呢?Wqw28资讯网——每日最新资讯28at.com

枚举,即 enum,可用于表示一组范围固定的值,它能助我们写出清晰、安全的代码。Wqw28资讯网——每日最新资讯28at.com

以编写游戏程序为一个简单案例:游戏中的角色有如战士、法师或者弓箭手,这种范围固定的值,就可以用枚举来表示。Wqw28资讯网——每日最新资讯28at.com

但 Go 中,枚举的表现方式不像在某些其他语言中那样直接。我们要想在 Go 中用好枚举,就要了解 Go 中枚举的不同表示形式和使用注意点。Wqw28资讯网——每日最新资讯28at.com

使用 iota 和常量

在 Go 中,使用 iota 和常量是最常见的表示枚举的方式。Wqw28资讯网——每日最新资讯28at.com

什么是 iota?Wqw28资讯网——每日最新资讯28at.com

iota 是 Go 中是一个非常特别的 Keyword,它可以帮助我们按一定规则创建一系列相关的常量,而无需手动为每个变量单独赋值。这一点与枚举的用途天然契合。Wqw28资讯网——每日最新资讯28at.com

不了解上面文字的含义?Wqw28资讯网——每日最新资讯28at.com

让我们来看一个例子,基于 iota 快速创建特定规则的常量。Wqw28资讯网——每日最新资讯28at.com

示例代码,如下所示:Wqw28资讯网——每日最新资讯28at.com

type Weekday intconst (    Sunday    Weekday = iota // 0    Monday                   // 1    Tuesday                  // 2    Wednesday                // 3    Thursday                 // 4    Friday                   // 5    Saturday                 // 6)

例子中,Weekday 类型有 7 个值,分别代表一周的七天。内部值从 0 开始,iota 自动增加赋值给每个常量,从 Sunday 到 Saturday 分别赋值 0-6。Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

现在,我们就不用手动给每个常量赋值。Wqw28资讯网——每日最新资讯28at.com

iota 还有很多骚操作,本文目标不在此,就不展开了。Wqw28资讯网——每日最新资讯28at.com

这种方法的优点是简单,提供了一定程度上类型安全,但它也有局限性。Wqw28资讯网——每日最新资讯28at.com

我觉得主要是两点不足。Wqw28资讯网——每日最新资讯28at.com

首先,对比其他语言的枚举,它不能直接从字符串转换到枚举类型,以上面代码为例,它不能从 "Sunday" 字符串转为 Sunday 枚举值。Wqw28资讯网——每日最新资讯28at.com

其次,它的类型安全不是绝对安全。Wqw28资讯网——每日最新资讯28at.com

如上的 Weekday 类型,我们虽不能将一个明确类型的变量赋值给 Weekday 类型变量:Wqw28资讯网——每日最新资讯28at.com

day := 0 // int// compiler: cannot use day (variable of type int) // as Weekday value in variable declaration [IncompatibleAssign]var sunday Weekday = day

但却可以将一个非 Weekday 类的字面量赋值给它。Wqw28资讯网——每日最新资讯28at.com

// 字面量 10 赋值给类型为 Weekday 的 day 变量var day Weekday = 10

很明显,10 这个数字并不在 Weekday 的有效范围,但却可以有效赋值而并没有报错。Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

如果是其他枚举机制完善的 enum 类型的语言,肯定是无法编译通过的。Wqw28资讯网——每日最新资讯28at.com

除了最基础的实现方式,我们继续看看还有哪些其他表示形式吧。Wqw28资讯网——每日最新资讯28at.com

支持字符串转化的枚举值

我们在开发 Web 应用时,常会遇到要将枚举值以字符串形式表示的需求,特别是在与前端对接时。那么,就让我们先尝试实现这一个需求,string 变量与枚举变量相互转化。Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

这个问题说来简单,Go 语言中,我们可采用字符串常量作为枚举值。Wqw28资讯网——每日最新资讯28at.com

示例代码,如下所示:Wqw28资讯网——每日最新资讯28at.com

type HttpMethod stringconst (    Get    HttpMethod = "GET"    Post   HttpMethod = "POST"    Put    HttpMethod = "PUT"    Delete HttpMethod = "DELETE")

这种方法简单直观,而且也易于与 JSON 等数据格式转换。Wqw28资讯网——每日最新资讯28at.com

type Request struct {    Method HttpMethod    URL    string}func main() {    r := Request{Method: Get, URL: "https://zhihu.com"}    result, _ := json.Marshal(r)    fmt.Printf("%s/n", result)}

输出:Wqw28资讯网——每日最新资讯28at.com

{"Method":"GET","URL":"https://zhihu.com"}

当如果我们还想保留原始的 iota 的整型枚举值,毕竟它更轻量,占用内存空少。这是否可以实现呢?我们尝试一下吧。Wqw28资讯网——每日最新资讯28at.com

定义如下:Wqw28资讯网——每日最新资讯28at.com

type HttpMethod intconst (    Get    HttpMethod = iota    Post    Put    Delete)

只要在枚举类型上增加整型值与字符串两者间相互转化的方法即可。Wqw28资讯网——每日最新资讯28at.com

代码如下所示:Wqw28资讯网——每日最新资讯28at.com

// 从 string 转为 HttpMethodfunc NewFromString(method string) HttpMethod {  switch h {  case "Get":    // 省略 ...  case "Delete":  default:    return Get // default is Get or error, panic  }}// 从 HttpMethod 转为 stringfunc (h HttpMethod) String() string {  switch h {  case Get:    return "Get"    // 省略 ...  default:    return "Unknown" // or error, panic  }}

我们实现从 string 构造 enum 方法,和从 enum 类型转为 string 的 String 方法。Wqw28资讯网——每日最新资讯28at.com

这里存在的一个问题,如果希望支持友好的 JSON 序列化反序列化的话,即枚举用字符串表示,则需要为 HttpMethod 新增方法,实现 json.Marshaler和json.Unmarshaler接口,自定义这个转化过程。Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

代码如下:Wqw28资讯网——每日最新资讯28at.com

// MarshalJSON 实现 json.Marshaler 接口func (h HttpMethod) MarshalJSON() ([]byte, error) {    return json.Marshal(h.String())}// UnmarshalJSON 实现 json.Unmarshaler 接口func (h *HttpMethod) UnmarshalJSON(data []byte) error {    var method string    if err := json.Unmarshal(data, &method); err != nil {        return err    }    *h = NewFromString(method)    return nil}

如果去找一些开源项目,可能会发现一些实现了这种 enum 的包,你只要通过 iota 定义枚举类型,从字符串和枚举间转化的代码可通过命令直接生成。Wqw28资讯网——每日最新资讯28at.com

robpike 开发过一个工具名为 stringer[1],可直接基于类似如上 HttpMethod 定义生成 String() 方法,不过它不是完整的 enum 支持。Wqw28资讯网——每日最新资讯28at.com

//go:generate stringer -type=HttpMethodtype HttpMethod intconst (    Get    HttpMethod = iota    Post    Put    Delete)

我们执行 go generate 即可为 HttpMethod 类型生成 String 方法。Wqw28资讯网——每日最新资讯28at.com

go generate

这里有个提前,要单独安装下 stringer 命令。Wqw28资讯网——每日最新资讯28at.com

不过,即使到现在,依然存在类型安全的问题,类似 var Hello HttpMethod = 10 这样的代码依然有效。Wqw28资讯网——每日最新资讯28at.com

继续吧!Wqw28资讯网——每日最新资讯28at.com

结构体枚举值

GO 中可基于结构体类型,实现枚举效果。Wqw28资讯网——每日最新资讯28at.com

举例说明,我们创建一个颜色的枚举,要求不仅有颜色的名字,还有颜色的 RGB 值。同时,为了方便记录,我们可以给它加上一个枚举整型值。Wqw28资讯网——每日最新资讯28at.com

type Color struct {    Index int    Name  string    RGB   string}

这样我们就有了一个颜色的枚举,每个颜色都有一个索引、名字和 RGB 值。Wqw28资讯网——每日最新资讯28at.com

如何使用呢?Wqw28资讯网——每日最新资讯28at.com

类似于前面的方式,我们直接定义,如下所示:Wqw28资讯网——每日最新资讯28at.com

var (      Red   = Color{0, "Red", "#FF0000"}      Green = Color{1, "Green", "#00FF00"}      Blue  = Color{2, "Blue", "#0000FF"})

这种方法比较灵活,但也相对复杂。Wqw28资讯网——每日最新资讯28at.com

好处也比较明显,如现在能存储的信息也更加丰富,前面类似于整型与字符串的各种转化都变的轻而易举了。我们直接整型数值 Color.Index、字符串 Color.Name。Wqw28资讯网——每日最新资讯28at.com

不过,如果要最大化与其他库结合,如自定义 JSON 转化规则,要实现 JSON 序列和反序列接口,字符串格式化要实现 Stringer 接口等。Wqw28资讯网——每日最新资讯28at.com

还有,这种结构其实不是常量类型的,就存在数据可更改的问题。不过,有这个安全需求的话,可考虑将成员字段私有化,通过方法变更即可。Wqw28资讯网——每日最新资讯28at.com

结构体类似命名空间效果

在网上看到个有点傻的设计,顺便也提一下吧。Wqw28资讯网——每日最新资讯28at.com

假设,我们有很多枚举类型,担心可能会出现命名冲突,可以用结构体来创建一个“命名空间”,把相关的枚举值组织在一起:Wqw28资讯网——每日最新资讯28at.com

示例代码如下所示:Wqw28资讯网——每日最新资讯28at.com

var Colors = struct {    Red, Green, Blue Color}{      Red   = Color{0, "Red", "#FF0000"}      Green = Color{1, "Green", "#00FF00"}      Blue  = Color{2, "Blue", "#0000FF"}}

上面的例子中定义了 Colors 变量,它是匿名结构体类型,字段名表示颜色,我们可通过 Colors.xxx 形式调用颜色。Wqw28资讯网——每日最新资讯28at.com

我初期看到这个写法,还以为限定了类型可定义的枚举值范围。发现其实不是,我依然可使用 Color 类型定义新值。Wqw28资讯网——每日最新资讯28at.com

这很不优雅,也很鸡肋,其实我完全可以新建 package 实现。不过既然发现了这个方案,就写到这里吧。Wqw28资讯网——每日最新资讯28at.com

类型安全?

到这里,其实所有实现方式都没有解决一个问题,那就是定义完枚举后,依然继续添加新的枚举值。Wqw28资讯网——每日最新资讯28at.com

我真的想实现这样的能力呢?该如何做呢?Wqw28资讯网——每日最新资讯28at.com

以前面 HttpMethod 为例,我要做的就是禁止通过 HttpMethod(1) 创建新枚举值。Wqw28资讯网——每日最新资讯28at.com

这不是很简单吗?Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

我们只要将枚举实现封装成一个 package,将类型小写,如 httpMethod,暴露它的类似 FromString 和其它函数,实现强制通过转化函数它。Wqw28资讯网——每日最新资讯28at.com

package httpmethodtype httpmethod stringconst (  Get  = "Get"  Post = "Post")func FromString(method string) httpmethod {  switch method {  case "Get":    return Get  case "Post":    return Post  }}

现在,枚举创建必须通过方法,我们就可以在其中实现限定创建规则。Wqw28资讯网——每日最新资讯28at.com

方法可能挺好,但好像没见到这么玩的?Wqw28资讯网——每日最新资讯28at.com

为什么呢?Wqw28资讯网——每日最新资讯28at.com

我的猜想是,开发时我们不会随意创建新的枚举值,对于边界数据的传递,确保通过转化函数处理不就行了吗?Wqw28资讯网——每日最新资讯28at.com

真实场景

对真实场景下枚举的使用,有价值之处主要在与其他系统的对接。Wqw28资讯网——每日最新资讯28at.com

图片图片Wqw28资讯网——每日最新资讯28at.com

举例而言,如来自前端 API 或数据库,有时可能出现一些异常值。对这类场景,通过前面介绍,可提供转化函数,在其中设置检查规则。如果发现异常选择丢弃,执行如 error 或 panic。Wqw28资讯网——每日最新资讯28at.com

而对于内部系统,如果使用类似于 protobuffer 协议,可在协议上限定好枚举范围,标记异常数据等。Wqw28资讯网——每日最新资讯28at.com

当然,可能出现因为发布时间次序或者兄弟团队忘记通知等,导致系统间枚举值对不齐的情况,也会按上面的逻辑丢弃、error 等,便于即使发现。Wqw28资讯网——每日最新资讯28at.com

对于团队合作这类场景,最好的解决方式,还是要在设计系统时,考虑上下游的兼容性,而不是每当有变动,全员乱糟糟,这最容易导致生产事故了。Wqw28资讯网——每日最新资讯28at.com

其实无论哪一种情况,重点在于保证进入系统的数据是否可通过转化检测,而不是多此一举,限制类似于 HttpMethod("Get") 的类型转化,因为没有人会这么写代码。Wqw28资讯网——每日最新资讯28at.com

总结

Go 语言中,枚举的表达方式多种多样。从简单的 iota 到复杂的结构体方式,每种方法都有其适用场景。作为开发者,最好是根据自己的具体需求,选择合适的实现方式。Wqw28资讯网——每日最新资讯28at.com

最后,希望这篇文章能帮助你在使用 Go 语言时,更加灵活且游刃有余地使用枚举 enum。Wqw28资讯网——每日最新资讯28at.com

博客地址:Go语言中 enum 实现方式有哪些?一定要类型安全吗?[2]Wqw28资讯网——每日最新资讯28at.com

引用链接

[1] stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringerWqw28资讯网——每日最新资讯28at.com

[2] Go语言中 enum 实现方式有哪些?一定要类型安全吗?: https://www.poloxue.com/posts/2024-02-02-how-to-use-enums-type-in-golang/Wqw28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-71945-0.htmlGo 语言中 enum 实现方式有哪些?一定要绝对类型安全吗?

声明:本网页内容旨在传播知识,不代表本站观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。

显示全文

上一篇:Nodejs - 九步开启JWT身份验证

下一篇:欧盟反垄断「撬开」苹果,结果坑苦了开发者

最新热点