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