文章

golang枚举的实践、原理与拓展

今天因为需求,好好研究了一下golang的枚举体系。网上的文章缺乏系统性,走了一些弯路。因此我自己整理一下。

golang存在枚举吗

如果你希望找到c++那样的强枚举,那么你会非常失望。因为go不仅没有强枚举,连弱枚举也没有。只有通过自增长常量实现的伪弱枚举。

通常会写成如下形式。

//ColumnType 列类型
type ColumnType int

const (
	CtNone         ColumnType                            = iota //无效
	CtInt                                                       //整数
	CtString                                                    //文本
	CtFloat                                                     //小数
	CtEnum                                                      //枚举
	CtBitEnum                                                   //位枚举
	CtVecDataPKey                                               //主枚举列
	CtVecDataCKey                                               //子枚举列
	CtVecDataValue                                              //数据列
	CtRealEnd                                                   //真实类型结束
	CtLogicBegin   = 100                                        //逻辑类型 为了避免增减枚举导致的版本不对应绕过此处,因此特殊处理一下
	CtData         = iota + CtLogicBegin - CtRealEnd - 1        // 压缩过后的逻辑数据
	CtAttrs                                                     // 属性集
)

一般如下使用

for i, col := range logic.Columns {
		// 非复杂类型
		switch col.ExcelType {
		case CtVecDataPKey, CtVecDataCKey:
			continue
		case CtVecDataValue:
			if i == len(logic.Columns)-1 ||
				(logic.Columns[i+1].ExcelType != CtVecDataPKey &&
					logic.Columns[i+1].ExcelType != CtVecDataCKey) {
				excel.ColumnTypes = append(excel.ColumnTypes, CtData)
			}
		case CtBitEnum:
			excel.ColumnTypes = append(excel.ColumnTypes, CtAttrs)
		default:
			excel.ColumnTypes = append(excel.ColumnTypes, col.ExcelType)
		}
	}

和弱枚举的功能和效果一样,同时也具备弱枚举的缺点,你无法避免用户传入不合理的值例如 CtEnumType(1111)。但是通过合理的caseif的判断是可以避免的。下文也会提到。

如何使用枚举

容错

必须要将默认值设为无效,如果你使用int作为枚举,那么应该手动制定0为无效值。避免忘记对枚举赋值,而产生意外的结果。

CtNone         ColumnType                            = iota //无效

需要指定有效区域,对区域外的枚举过滤掉。

CtInt                                                       //整数
CtString                                                    //文本
CtFloat                                                     //小数
CtEnum                                                      //枚举
CtBitEnum                                                   //位枚举
CtVecDataPKey                                               //主枚举列
CtVecDataCKey                                               //子枚举列
CtVecDataValue                                              //数据列
CtRealEnd                                                   //真实类型结束
if cType == CtNone || cType >= CtRealEnd {
	return
}

使用iota

iota 是go提供的枚举语法糖,可以在const定义块中提供以某种规律连续分布的值。

最常见的使用方法是

const (
  a = iota
	b
	c
	d
)

此时,abcd的值依次为0、1、2、3。

也可以将iota放在公式里面,值也会顺延

a = iota * 2
b
c
d

此时abcd的值依次为0、2、4、6

通过这个方式我们可以实现位枚举

a = 1 << iota
b
c
d

值依次为 1、2、4、8。

itoa可以在过程中被重新指定公式,但是值还是连续分布的

a = iota
b
c
d
e = 1 << iota

值依次为 0、1、2、3、16

有些人可能会想,我们能不能和c的枚举一样跳过某些值或者指定某一个枚举后,枚举又从另外一个值开始呢。于是写出了以下c风格代码。

a = iota
b
c
d = 100 // 以下是另外一组
e
f

实际上值依次为0、1、2、100、100、100。是的,后面三个枚举的值都是100,同时goland也无法识别出值对应的枚举。于是有人尝试了以后写成了

a = ioat
b
c
d = 100 + ioat // 以下是另外一组
e
f

这个代码是可以正常运作的,但是无法以你想象的方式运行。实际上值依次为0、1、2、103、104、105。本质上来看itoa的值没有重新指定。此时goland也无法将103识别为d,不过我个人认为这个是goland的bug。

正确的写法应该是

a = ioat
b
c
normalEnd
otherBegin = 100 + ioat - normalEnd // 以下是另外一组
d
e

此时的逻辑是正确的

abcde的值依次为0、1、2、101、102,就是有那么一点扭曲。

同时go还提供了一个小语法糖 _

a = ioat
_
_
b

此时ab的值依次为0、3。

代码规范

我个人建议为了避免造成误解,枚举变量以e开头。枚举值以枚举类型的缩写开头例如。

var eType ColumnType = CtInt  

这和指针需要以p开头、接口以i开头一样,看上去是过时的匈牙利命名法。但是在go这个自由且泛用的环境下可以避免很多问题。

License:  CC BY 4.0