嗨!大家好,本文 Go 语言小技巧系列的第十二篇,往期文章查看:Go 语言小技巧。
你是否了解过 Go 中的枚举呢?
枚举,即 enum,可用于表示一组范围固定的值,它能助我们写出清晰、安全的代码。
以编写游戏程序为一个简单案例:游戏中的角色有如战士、法师或者弓箭手,这种范围固定的值,就可以用枚举来表示。
但 Go 中,枚举的表现方式不像在某些其他语言中那样直接。我们要想在 Go 中用好枚举,就要了解 Go 中枚举的不同表示形式和使用注意点。
在 Go 中,使用 iota 和常量是最常见的表示枚举的方式。
什么是 iota?
iota 是 Go 中是一个非常特别的 Keyword,它可以帮助我们按一定规则创建一系列相关的常量,而无需手动为每个变量单独赋值。这一点与枚举的用途天然契合。
不了解上面文字的含义?
让我们来看一个例子,基于 iota 快速创建特定规则的常量。
示例代码,如下所示:
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。
图片
现在,我们就不用手动给每个常量赋值。
iota 还有很多骚操作,本文目标不在此,就不展开了。
这种方法的优点是简单,提供了一定程度上类型安全,但它也有局限性。
我觉得主要是两点不足。
首先,对比其他语言的枚举,它不能直接从字符串转换到枚举类型,以上面代码为例,它不能从 "Sunday" 字符串转为 Sunday 枚举值。
其次,它的类型安全不是绝对安全。
如上的 Weekday 类型,我们虽不能将一个明确类型的变量赋值给 Weekday 类型变量:
day := 0 // int// compiler: cannot use day (variable of type int) // as Weekday value in variable declaration [IncompatibleAssign]var sunday Weekday = day
但却可以将一个非 Weekday 类的字面量赋值给它。
// 字面量 10 赋值给类型为 Weekday 的 day 变量var day Weekday = 10
很明显,10 这个数字并不在 Weekday 的有效范围,但却可以有效赋值而并没有报错。
图片
如果是其他枚举机制完善的 enum 类型的语言,肯定是无法编译通过的。
除了最基础的实现方式,我们继续看看还有哪些其他表示形式吧。
我们在开发 Web 应用时,常会遇到要将枚举值以字符串形式表示的需求,特别是在与前端对接时。那么,就让我们先尝试实现这一个需求,string 变量与枚举变量相互转化。
图片
这个问题说来简单,Go 语言中,我们可采用字符串常量作为枚举值。
示例代码,如下所示:
type HttpMethod stringconst ( Get HttpMethod = "GET" Post HttpMethod = "POST" Put HttpMethod = "PUT" Delete HttpMethod = "DELETE")
这种方法简单直观,而且也易于与 JSON 等数据格式转换。
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)}
输出:
{"Method":"GET","URL":"https://zhihu.com"}
当如果我们还想保留原始的 iota 的整型枚举值,毕竟它更轻量,占用内存空少。这是否可以实现呢?我们尝试一下吧。
定义如下:
type HttpMethod intconst ( Get HttpMethod = iota Post Put Delete)
只要在枚举类型上增加整型值与字符串两者间相互转化的方法即可。
代码如下所示:
// 从 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 方法。
这里存在的一个问题,如果希望支持友好的 JSON 序列化反序列化的话,即枚举用字符串表示,则需要为 HttpMethod 新增方法,实现 json.Marshaler和json.Unmarshaler接口,自定义这个转化过程。
图片
代码如下:
// 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 定义枚举类型,从字符串和枚举间转化的代码可通过命令直接生成。
robpike 开发过一个工具名为 stringer[1],可直接基于类似如上 HttpMethod 定义生成 String() 方法,不过它不是完整的 enum 支持。
//go:generate stringer -type=HttpMethodtype HttpMethod intconst ( Get HttpMethod = iota Post Put Delete)
我们执行 go generate 即可为 HttpMethod 类型生成 String 方法。
go generate
这里有个提前,要单独安装下 stringer 命令。
不过,即使到现在,依然存在类型安全的问题,类似 var Hello HttpMethod = 10 这样的代码依然有效。
继续吧!
GO 中可基于结构体类型,实现枚举效果。
举例说明,我们创建一个颜色的枚举,要求不仅有颜色的名字,还有颜色的 RGB 值。同时,为了方便记录,我们可以给它加上一个枚举整型值。
type Color struct { Index int Name string RGB string}
这样我们就有了一个颜色的枚举,每个颜色都有一个索引、名字和 RGB 值。
如何使用呢?
类似于前面的方式,我们直接定义,如下所示:
var ( Red = Color{0, "Red", "#FF0000"} Green = Color{1, "Green", "#00FF00"} Blue = Color{2, "Blue", "#0000FF"})
这种方法比较灵活,但也相对复杂。
好处也比较明显,如现在能存储的信息也更加丰富,前面类似于整型与字符串的各种转化都变的轻而易举了。我们直接整型数值 Color.Index、字符串 Color.Name。
不过,如果要最大化与其他库结合,如自定义 JSON 转化规则,要实现 JSON 序列和反序列接口,字符串格式化要实现 Stringer 接口等。
还有,这种结构其实不是常量类型的,就存在数据可更改的问题。不过,有这个安全需求的话,可考虑将成员字段私有化,通过方法变更即可。
在网上看到个有点傻的设计,顺便也提一下吧。
假设,我们有很多枚举类型,担心可能会出现命名冲突,可以用结构体来创建一个“命名空间”,把相关的枚举值组织在一起:
示例代码如下所示:
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 形式调用颜色。
我初期看到这个写法,还以为限定了类型可定义的枚举值范围。发现其实不是,我依然可使用 Color 类型定义新值。
这很不优雅,也很鸡肋,其实我完全可以新建 package 实现。不过既然发现了这个方案,就写到这里吧。
到这里,其实所有实现方式都没有解决一个问题,那就是定义完枚举后,依然继续添加新的枚举值。
我真的想实现这样的能力呢?该如何做呢?
以前面 HttpMethod 为例,我要做的就是禁止通过 HttpMethod(1) 创建新枚举值。
这不是很简单吗?
图片
我们只要将枚举实现封装成一个 package,将类型小写,如 httpMethod,暴露它的类似 FromString 和其它函数,实现强制通过转化函数它。
package httpmethodtype httpmethod stringconst ( Get = "Get" Post = "Post")func FromString(method string) httpmethod { switch method { case "Get": return Get case "Post": return Post }}
现在,枚举创建必须通过方法,我们就可以在其中实现限定创建规则。
方法可能挺好,但好像没见到这么玩的?
为什么呢?
我的猜想是,开发时我们不会随意创建新的枚举值,对于边界数据的传递,确保通过转化函数处理不就行了吗?
对真实场景下枚举的使用,有价值之处主要在与其他系统的对接。
图片
举例而言,如来自前端 API 或数据库,有时可能出现一些异常值。对这类场景,通过前面介绍,可提供转化函数,在其中设置检查规则。如果发现异常选择丢弃,执行如 error 或 panic。
而对于内部系统,如果使用类似于 protobuffer 协议,可在协议上限定好枚举范围,标记异常数据等。
当然,可能出现因为发布时间次序或者兄弟团队忘记通知等,导致系统间枚举值对不齐的情况,也会按上面的逻辑丢弃、error 等,便于即使发现。
对于团队合作这类场景,最好的解决方式,还是要在设计系统时,考虑上下游的兼容性,而不是每当有变动,全员乱糟糟,这最容易导致生产事故了。
其实无论哪一种情况,重点在于保证进入系统的数据是否可通过转化检测,而不是多此一举,限制类似于 HttpMethod("Get") 的类型转化,因为没有人会这么写代码。
Go 语言中,枚举的表达方式多种多样。从简单的 iota 到复杂的结构体方式,每种方法都有其适用场景。作为开发者,最好是根据自己的具体需求,选择合适的实现方式。
最后,希望这篇文章能帮助你在使用 Go 语言时,更加灵活且游刃有余地使用枚举 enum。
博客地址:Go语言中 enum 实现方式有哪些?一定要类型安全吗?[2]
[1] stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer
[2] Go语言中 enum 实现方式有哪些?一定要类型安全吗?: https://www.poloxue.com/posts/2024-02-02-how-to-use-enums-type-in-golang/
本文链接:http://www.28at.com/showinfo-26-71945-0.htmlGo 语言中 enum 实现方式有哪些?一定要绝对类型安全吗?
声明:本网页内容旨在传播知识,不代表本站观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。