Go语言精进之路:从新手到高手的编程思想、方法和技巧(1)
上QQ阅读APP看书,第一时间看更新

第10条 使用iota实现枚举常量

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含义的例子,我们逐行来看。

  • mutexLocked = 1 << iota:这里是const声明块的第一行,iota的值是该行在const块中的偏移量,因此iota的值为0,我们得到mutexLocked这个常量的值为1 << 0,即1。
  • mutexWoken:这里是const声明块的第二行,由于没有显式的常量初始化表达式,根据const声明块的“隐式重复前一个非空表达式”机制,该行等价于mutexWoken = 1 << iota。由于该行是const块中的第二行,因此偏移量iota的值为1,我们得到mutexWoken这个常量的值为1<< 1,即2。
  • mutexStarving:该常量同mutexWoken,该行等价于mutexStarving = 1 << iota,由于在该行的iota的值为2,因此我们得到mutexStarving这个常量的值为 1 << 2,即4。
  • mutexWaiterShift = iota:这一行的常量初始化表达式与前三行不同,由于该行为第四行,iota的偏移值为3,因此mutexWaiterShift的值就为3。

位于同一行的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
)