Go语言编程教程-枚举

课程要点

  1. 了解Golang中的枚举
  2. 自定义枚举值
  3. 跳过某个枚举值
  4. 枚举的常用惯例
  5. 了解fmt.Stringer接口

Golang中的枚举

在Golang中并没有像其他语言一样,拥有类似于enum的常规枚举类型,而是通过使用一组常量来实现类似枚举的功能。

如下所示,我们定义了三个常量来表示状态语义的枚举值

const (
    StatusSuccess   = 0
    StatusFailed    = 1
    StatusForbidden = 2
)

在Golang项目开发中我们就可以直接使用上面的方式来代表我们的自定义枚举语义。
观察上面的示例以及结合日常的开发经验,可以得到我们的枚举值通常就是一组连续的数字。

所以在Golang中增加了一个iota关键字来简化上面的写法,使我们的代码更加简洁,如下所示

const (
    StatusSuccess = iota // 0
    StatusFailed         // 1
    StatusForbidden      // 2
)

上面的代码与我们之前直接定义常量是等效的,这里的iota相当于一个从0开始的累加器,每一个枚举值都会按照顺序依次加1,
从而接管了我们手动管理枚举值的工作,使代码更加简洁。

换一种更加容易理解的说法,可以把这一组枚举项当作一个数组集合,这里的iota可以理解为每个枚举项所在的索引值。
所以上述代码与下面的代码也是等效的。

const (
    StatusSuccess   = iota // 0
    StatusFailed    = iota // 1
    StatusForbidden = iota // 2
)

所以为了保持代码的简洁,我们只需要保留第一个iota即可。

自定义枚举值

通常情况下,我们的枚举值都是从0开始依次递增的,但是有少数情况下,我们也会有需要自定义枚举值的需求。

比如,我需要枚举值从1开始,而不是0,就可以像下面这么写

const (
    StatusSuccess = iota + 1 // 1
    StatusFailed             // 2
    StatusForbidden          // 3
)

上面的代码可能会让你看着有点疑惑,那我们把代码补全再看,可能会更清晰些

const (
    StatusSuccess   = iota + 1 // 1
    StatusFailed    = iota + 1 // 2
    StatusForbidden = iota + 1 // 3
)

这下是不是就很容易理解了,上面两段代码是等效的,每一个没有被显式赋值的枚举项,都会依次往上寻找最近的一个显示定义的枚举项,
使用它的枚举值表达式作为当前枚举项的值,也就是iota + 1

因为iota等价于枚举项的索引位置,所以上述示例代码的枚举值会出现从1开始逐步递增的效果。

那么举一反三一下,如果我们需要将每个枚举值之间的增长步长由1改为2该怎么实现呢?

聪明的你肯定已经想到了,没错就是将第一个值设置为iota * 2即可,如下所示

const (
    StatusSuccess   = iota * 2 // 0
    StatusFailed               // 2
    StatusForbidden            // 4
)

那如果我们将第一个枚举值设置为一个常量的话会出现什么情况呢?

参考前文所说的,我们很容易就能得出答案:
每一个没有被显式赋值的枚举项,都会依次往上寻找最近的一个显示定义的枚举项,使用它的枚举值表达式作为当前枚举项的值

没错,假设我们将第一个枚举值设置为1,那么后续没有显式赋值的枚举都会参照第一个枚举的表达式,也就是字面量1,如下所示:

const (
    StatusSuccess   = 1 // 1
    StatusFailed        // 1
    StatusForbidden     // 1
)

至此,你应该对Golang的枚举以及iota表达的含义有了比较清晰的认识了,
虽然一开始可能会有点迷糊,但是如果你亲自尝试了所有示例,所有疑惑都会轻易解开。

跳过某个枚举值

在实际开发过程中,我们在定义枚举的时候,可能需要为程序预留一些枚举值,暂时还不会使用,此时我们该怎么办呢?

通常我们可能会先任意定义一批枚举值,并加上一些注释,告诉开发者这是预留的,请暂时不要使用之类的。

显而易见,这是比较糟糕的做法,因为并不是所有人都会认真的去看注释并严格的去遵守相应的规则。

万幸的是,Golang的开发者已经想到了这一点,并为我们提供了一个极其简单的方式来处理这个问题。

我们可以使用_作为一个枚举项来表示一个占位,它会拥有自己的枚举数值,但是任何人都不能通过它的名称来进行调用,
也就是说枚举项_对任何人都是不可见的,所以它可以多次使用,如下所示:

const (
    StatusSuccess   = iota // 0
    StatusFailed           // 1
    StatusForbidden        // 2
    _                      // 3
    _                      // 4
)

妙哉,没有比这更简单的了吧?

枚举的常用惯例

因为Golang的枚举是用数字常量来实现的,所以它携带的信息非常少,我们很难理解某一个枚举数值代表着什么含义,
并且我们也很难去限定枚举的取值范围。

自定义枚举类型

所以为了使枚举更加好用,能够携带更多的信息,我们常用的一个做法是以int作为基础类型来自定义一个枚举类型,
从而为其扩展更多的功能,如下所示

type Status int

此时我们用自定义的类型Status来创建枚举,如下所示

const (
    Success   Status = iota // 0
    Failed                  // 1
    Forbidden               // 2
    _                       // 3
    _                       // 4
)

从上面可以看到,对比之前的例子,唯一的区别就是为第一个枚举指定了一个显式类型Status,如果未指定类型,则它的默认类型是int

此时所有枚举项的类型都变成Status了,因为Status的基础类型是int,所以之前的规则依旧有效,完全与之前所说的相兼容。

枚举边界

另外,在使用枚举的过程中,我们经常会通过一些边界值来判断枚举值的有效性,
比如判断用户传入的枚举值是否小于枚举定义的最小值或者大于枚举定义的最大值,如果不符合条件,则传入的枚举不合法。

亦或者在一批枚举值中存在着更加细分语义的分组,比如有一批状态码表示不同的成功语义,另一批状态码表示不同的失败语义等等。

一种常见的做法就是直接使用对应的边界值来进行判断,但是这样存在一个问题,因为随着枚举值的增加或者减少,边界值可能发生变化。

比如我往后增加了一个枚举,那么当前枚举的最大值就变成新增的这个枚举了,所以校验枚举值有效性的方法就必须得修改了。

诸如此类,因为枚举项的变化,很多校验逻辑都会随之变化,对代码来说是一种潜在的风险,我们很可能会忘记修改某个边界判断逻辑。

所以聪明的开发者就想了一个办法,就是通过在枚举中定义一些私有的枚举项来作为枚举的边界值,
也就是将枚举项定义为以小写字母或者_开头的常量,这样外部包就无法看到和使用这些私有的边界值了。

在我们当前包的代码中,就可以通过这些边界值来控制我们上述遇到的边界判断问题,而且当我们的枚举项发生增减的时候,
我们的边界逻辑也不需要随之变更,完整示例如下所示

const (
    minStatus Status = iota // 0
    Success                 // 1
    Failed                  // 2
    Forbidden               // 3
    _                       // 4
    _                       // 5
    maxStatus               // 6
)

fmt.Stringer接口

因为枚举归根到底其实是一个整型数字,所以在实际运行的时候,比如在打印日志时,我们很难理解具体的数字代表着什么含义,
如果每次都要去查看相应的文档可真的太费劲了。

我们可以使用fmt.Printf来尝试打印枚举的信息,这里我们之前定义的枚举值边界就派上用场了,可以用它来循环遍历所有的枚举项

func main() {
    for status := minStatus; status <= maxStatus; status++ {
        fmt.Printf("%d -> %v\n", status, status)
    }
}

输出结果如下,可以看到根据打印结果很难判断其内在的含义

0 -> 0
1 -> 1
2 -> 2
3 -> 3
4 -> 4
5 -> 5
6 -> 6

幸好Golang为我们提供了一个fmt.Stringer接口,我们只要实现这个接口,当我们使用fmt包进行格式化输出的时候,
就会调用对应的方法输出更加有用的自定义信息。

fmt.Stringer接口中只有一个String() string方法,我们只要实现该方法即可,
如下所示,我们通过当前的枚举值直接返回其对应的描述信息

func (s Status) String() string {
    switch s {
    case Success:
        return "Success"
    case Failed:
        return "Failed"
    case Forbidden:
        return "Forbidden"
    default:
        return "Unknown"
    }
}

最后我们再次运行我们的代码,可以看到从输出结果中很容易就能知道每个枚举值的具体含义了

0 -> Unknown
1 -> Success  
2 -> Failed   
3 -> Forbidden
4 -> Unknown  
5 -> Unknown  
6 -> Unknown  

小结

通过本章的学习,我们了解了Golang中枚举的用法,它不像常规的编程语言一样拥有独立的枚举类型,而是通过常量配合上iota
关键字来实现枚举的功能。

经过上述一系列的优化,我们的枚举功能已经越来越完善了,当然这都不是必须的,
开发者可以根据实际的需求来定制自己的枚举写法,可以使用最精简的写法来定义枚举,也可以为其添加更加强大的功能,
所有的选择权都在开发者自己手中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容