go基础05-常量:Go在“常量”设计上的创新有哪些?

220 阅读6分钟

Go 语言在常量方面的创新包括下面这几点:

  • 支持无类型常量;
  • 支持隐式自动转型;
  • 可用于实现枚举。

Go 语言的常量是一种在源码编译期间被创建的语法元素。这是在说这个元素的值可以像变量那样被初始化,但它的初始化表达式必须是在编译期间可以求出值来的。

我们前面学过,Go 是使用 var 关键字声明变量的。在常量这里,Go 语言引入 const 关键字来声明常量。而且,和 var 支持单行声明多个变量,以及以代码块形式聚合变量声明一样,const 也支持单行声明多个常量,以及以代码块形式聚合常量声明的形式,具体你可以看下面这个示例代码:

const Pi float64 = 3.14159265358979323846 // 单行常量声明

// 以const代码块形式声明常量
const (
    size int64 = 4096
    i, j, s = 13, 14, "bar" // 单行声明多个常量
)

Go 语言规范规定,Go 常量的类型只局限于前面我们学过的 Go 基本数据类型,包括数值类型、字符串类型,以及只有两个取值(true 和 false)的布尔类型。

无类型常量

type myInt int
const n myInt = 13
const m int = n + 5 // 编译器报错:cannot use n + 5 (type myInt) as type int in const initializer

func main() {
    var a int = 5
    fmt.Println(a + n) // 编译器报错:invalid operation: a + n (mismatched types int and myInt)
}

有类型常量与变量混合在一起进行运算求值的时候,也必须遵守类型相同这一要求,否则我们只能通过显式转型才能让上面代码正常工作,比如下面代码中,我们就必须通过将常量 n 显式转型为 int 后才能参与后续运算:

type myInt int
const n myInt = 13
const m int = int(n) + 5  // OK

func main() {
    var a int = 5
    fmt.Println(a + int(n))  // 输出:18
}

那么在 Go 语言中,只有这一种方法能让上面代码编译通过、正常运行吗 ?当然不是,我们也可以使用 Go 中的无类型常量来实现,你可以看看这段代码:

type myInt int
const n = 13

func main() {
    var a myInt = 5
    fmt.Println(a + n)  // 输出:18
}

你可以看到,在这个代码中,常量 n 在声明时并没有显式地被赋予类型,在 Go 中,这样的常量就被称为无类型常量(Untyped Constant)。

不过,无类型常量也不是说就真的没有类型,它也有自己的默认类型,不过它的默认类型是根据它的初值形式来决定的。像上面代码中的常量 n 的初值为整数形式,所以它的默认类型为 int。

隐式转型

隐式转型说的就是,对于无类型常量参与的表达式求值,Go 编译器会根据上下文中的类型信息,把无类型常量自动转换为相应的类型后,再参与求值计算,这一转型动作是隐式进行的。但由于转型的对象是一个常量,所以这并不会引发类型安全问题,Go 编译器会保证这一转型的安全性。

const m = 1333333333

var k int8 = 1
j := k + m // 编译器报错:constant 1333333333 overflows int8

这个代码中常量 m 的值 1333333333 已经超出了 int8 类型可以表示的范围,所以我们将它转换为 int8 类型时,就会导致编译器报溢出错误。

实现枚举

在 Go 语言中,我们可以使用 const 代码块定义的常量集合,来实现枚举。这是因为,枚举类型本质上就是一个由有限数量常量所构成的集合,所以这样做并没有什么问题。

首先,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 是 Go 语言的一个预定义标识符,它表示的是 const 声明块(包括单行声明)中,每个常量所处位置在块中的偏移值(从零开始)。同时,每一行中的 iota 自身也是一个无类型常量,可以像前面我们提到的无类型常量那样,自动参与到不同类型的求值过程中来,不需要我们再对它进行显式转型操作。

// $GOROOT/src/sync/mutex.go 
const ( 
    mutexLocked = 1 << iota
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    starvationThresholdNs = 1e6
)

首先,这个 const 声明块的第一行是 mutexLocked = 1 << iota ,iota 的值是这行在 const 块中的偏移,因此 iota 的值为 0,我们得到 mutexLocked 这个常量的值为 1 << 0,也就是 1。

接着,第二行:mutexWorken 。因为这个 const 声明块中并没有显式的常量初始化表达式,所以我们根据 const 声明块里“隐式重复前一个非空表达式”的机制,这一行就等价于 mutexWorken = 1 << iota。而且,又因为这一行是 const 块中的第二行,所以它的偏移量 iota 的值为 1,我们得到 mutexWorken 这个常量的值为 1 << 1,也就是 2。

然后是 mutexStarving。这个常量同 mutexWorken 一样,这一行等价于 mutexStarving = 1 << iota。而且,也因为这行的 iota 的值为 2,我们可以得到 mutexStarving 这个常量的值为 1 << 2,也就是 4;

再然后我们看 mutexWaiterShift = iota 这一行,这一行为常量 mutexWaiterShift 做了显式初始化,这样就不用再重复前一行了。由于这一行是第四行,而且作为行偏移值的 iota 的值为 3,因此 mutexWaiterShift 的值就为 3。

而最后一行,代码中直接用了一个具体值 1e6 给常量 starvationThresholdNs 进行了赋值,那么这个常量值就是 1e6 本身了。

看完这个例子的分析,我相信你对于 iota 就会有更深的理解了。不过我还要提醒你的是,位于同一行的 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
const (
    _ = iota
    IPV6_V6ONLY  // 1
    SOMAXCONN    // 2
    SO_ERROR     // 3
)

那如果我们的枚举常量值并不连续,而是要略过某一个或几个值,又要怎么办呢?我们也可以借助空白标识符来实现,如下面这个代码:

const (
    _ = iota // 0
    Pin1
    Pin2
    Pin3
    _
    Pin5    // 5   
)

比如我们使用传统的枚举常量声明方式,来声明一组按首字母排序的“颜色”常量,也就是这样:

const ( 
    Black  = 1 
    Red    = 2
    Yellow = 3
)

假如这个时候,我们要增加一个新颜色 Blue。那根据字母序,这个新常量应该放在 Red 的前面呀。但这样一来,我们就需要像下面代码这样将 Red 到 Yellow 的常量值都手动加 1,十分费力。

const (
    Blue   = 1
    Black  = 2
    Red    = 3
    Yellow = 4
)

那如果我们使用 iota 重新定义这组“颜色”枚举常量是不是可以更方便呢?我们可以像下面代码这样试试看:

const (
    _ = iota     
    Blue
    Red 
    Yellow     
) 

这样,无论后期我们需要增加多少种颜色,我们只需将常量名插入到对应位置就可以,其他就不需要再做任何手工调整了。

而且,如果一个 Go 源文件中有多个 const 代码块定义的不同枚举,每个 const 代码块中的 iota 也是独立变化的,也就是说,每个 const 代码块都拥有属于自己的 iota,如下面代码所示:

const (
    a = iota + 1 // 1, iota = 0
    b            // 2, iota = 1
    c            // 3, iota = 2
)

const (
    i = iota << 1 // 0, iota = 0
    j             // 2, iota = 1
    k             // 4, iota = 2
)

你可以看到,每个 iota 的生命周期都始于一个 const 代码块的开始,在该 const 代码块结束时结束。

此文章为3月Day5学习笔记,内容来源于极客时间《Tony Bai · Go 语言第一课》。