Go语言编程教程4-枚举

发布时间 2023-12-14 22:01:46作者: 陌上荼靡

课程要点

  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
关键字来实现枚举的功能。

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