



C家族的主流编程语言(如C++、Java等)都提供定义枚举常量的语法。比如在C语言中,枚举是一个具名的整型常数的集合。下面是使用枚举定义的Weekday类型:
// C语法
enum Weekday {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
};
int main() {
enum Weekday d = SATURDAY;
printf("%d\n", d); // 6
}
C语言针对枚举类型提供了很多语法上的便利,比如:如果没有显式给枚举常量赋初始值,那么枚举类型的第一个常量的值为0,后续常量的值依次加1。与使用define宏定义的常量相比,C编译器可以对专用的枚举类型进行严格的类型检查,使得程序更为安全。
枚举的存在代表了一类现实需求:
与其他C家族主流语言(如C++、Java)不同,Go语言没有提供定义枚举常量的语法。我们通常使用常量语法定义枚举常量,比如要在Go中定义上面的Weekday类型,可以这样写:
const (
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
)
如果仅仅能支持到这种程度,那么Go就算不上是“站在巨人的肩膀上”了。Go的const语法提供了“隐式重复前一个非空表达式”的机制,来看下面的代码:
const (
Apple, Banana = 11, 22
Strawberry, Grape
Pear, Watermelon
)
常量定义的后两行没有显式给予初始赋值,Go编译器将为其隐式使用第一行的表达式,这样上述定义等价于:
const (
Apple, Banana = 11, 22
Strawberry, Grape = 11, 22
Pear, Watermelon = 11, 22
)
不过这显然仍无法满足枚举的要求,Go在这个机制的基础上又提供了神器 iota 。有了iota,我们就可以定义满足各种场景的枚举常量了。
iota是Go语言的一个预定义标识符,它表示的是const声明块(包括单行声明)中每个常量所处位置在块中的偏移值(从零开始)。同时,每一行中的iota自身也是一个无类型常量,可以像无类型常量那样自动参与不同类型的求值过程,而无须对其进行显式类型转换操作。
下面是Go标准库中sync/mutex.go中的一段枚举常量的定义:
// $GOROOT/src/sync/mutex.go (go 1.12.7)
const (
mutexLocked = 1 << iota
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
这是一个很典型的诠释iota含义的例子,我们逐行来看。
位于同一行的iota即便出现多次,其值也是一样的:
const (
Apple, Banana = iota, iota + 10 // 0, 10 (iota = 0)
Strawberry, Grape // 1, 11 (iota = 1)
Pear, Watermelon // 2, 12 (iota = 2)
)
如果要略过iota = 0,而从iota = 1开始正式定义枚举常量,可以效仿下面的代码:
// $GOROOT/src/syscall/net_js.go,go 1.12.7
const (
_ = iota
IPV6_V6ONLY // 1
SOMAXCONN // 2
SO_ERROR // 3
)
如果要定义非连续枚举值,也可以使用类似方式略过某一枚举值:
const (
_ = iota // 0
Pin1
Pin2
Pin3
_ // 相当于_ = iota,略过了4这个枚举值
Pin5 // 5
)
iota的加入让Go在枚举常量定义上的表达力大增,主要体现在如下几方面。
(1)iota预定义标识符能够以更为灵活的形式为枚举常量赋初值
Go提供的iota预定义标识符可以参与常量初始化表达式的计算,这样我们能够以更为灵活的形式为枚举常量赋初值,而传统C语言的枚举仅能以已经定义了的常量参与到其他常量的初始值表达式中。比如:
// C代码
enum Season {
spring,
summer = spring + 2,
fall = spring + 3,
winter = fall + 1
};
在阅读上面这段C代码时,如果要对winter进行求值,我们还要向上查询fall的值和spring的值。
(2)Go的枚举常量不限于整型值,也可以定义浮点型的枚举常量
C语言无法定义浮点类型的枚举常量,但Go语言可以,这要归功于Go无类型常量。
const (
PI = 3.1415926 // π
PI_2 = 3.1415926 / (2 * iota) // π/2
PI_4 // π/4
)
(3)iota使得维护枚举常量列表更容易
我们使用传统的枚举常量声明方式声明一组颜色常量:
const (
Black = 1
Red = 2
Yellow = 3
)
常量按照首字母顺序排序。假如我们要增加一个颜色Blue,根据字母序,这个新常量应该放在Red的前面,但这样一来,我们就需要手动将从Red开始往后的常量的值都加1,十分费力。
const (
Blue = 1
Black = 2
Red = 3
Yellow = 4
)
我们使用iota重新定义这组颜色枚举常量:
const (
_ = iota
Blue
Black
Red
Yellow
)
现在无论后期增加多少种颜色,我们只需将常量名插入对应位置即可,无须进行任何手工调整。
(4)使用有类型枚举常量保证类型安全
枚举常量多数是无类型常量,如果要严格考虑类型安全,也可以定义有类型枚举常量。下面是Go标准库中一段定义有类型枚举常量的例子:
// $GOROOT/src/time/time.go
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
这样,后续要使用Sunday、Saturday这些有类型枚举常量时,必须匹配Weekday类型的变量。
最后,举一个“反例”:在一些枚举常量名称与其初始值有强烈对应关系的时候,枚举常量会直接使用显式数值作为常量的初始值。这样的情况极其少见,我在Go标准库中仅找到这一处:
// $GOROOT/bytes/buffer.go
const (
opRead readOp = -1
opInvalid readOp = 0
opReadRune1 readOp = 1
opReadRune2 readOp = 2
opReadRune3 readOp = 3
opReadRune4 readOp = 4
)