Golang 常量声明还能这么用?

1,938 阅读4分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

从群里的一个问题说起

今天在写 bug 的时候发现公司群里有同事问了这么个问题:

package main

import "fmt"

const (
	a = iota + 1
	b
	c
)

func main() {
	fmt.Println(a, b, c)
}

这个程序的运行结果很奇怪:

image.png

为啥打印出来是【1 2 3】而不是【1 1 2】?

这个很有意思,问题不复杂,原理也简单,但很多同学不熟悉,可能一时之间也答不上来原因。

这个问题需要的知识点有两个:

  • iota 的声明规则;
  • 常量声明规则。

这两个都是在编译器生效的,一定意义上属于 Golang 给开发者约定俗成的一些规范,我们也可以在 spec 中找到相关说明。今天咱们就来看看,这是个什么原因。

iota

能问出这个问题,说明这位同事还是有一些 iota 的意识的,我们回忆一下,之前的文章里有提过 Golang iota 用法解析:

Within a constant declaration, the predeclared identifier iota represents successive untyped integer constants. Its value is the index of the respective ConstSpec in that constant declaration, starting at zero.

什么意思呢?

翻译一下:iota 是一个 Golang 预声明的标识符,在一个常量定义段中,iota 代表了一组 连续的,无类型的,整数常量,它的值等于它在这个常量定义段中的序号(或者说行号,从 0 开始)。

所以

const (
    LocationShady    = iota  //0
    LocationWalnut           //1
    LocationKenmawr          //2
)

这里 LocationShady 是这个 const 定义段中的第一行,也是 iota 第一次出现,所以值为 1,后面两个在 Golang 看来也都是 iota 表达式,所以随着行号增加,变成了 1 和 2。

那如果我们再这个定义段上面加个别的定义呢?我们把一开始示例代码改成这样:

package main

import "fmt"

const (
	haha = "string"
	a    = iota + 1
	b
	c
)

func main() {
	fmt.Println(a, b, c)
}

执行后你会发现,现在会打印出来【2 3 4】,显然,随着行号增加,iota 表达的值也增加了,即便前面的声明中没有 iota 出现。

那如果我声明了多个,还有空行呢?我们再改一下:

package main

import "fmt"

const (
	ceshi = 9920
	haha  = "string"

	a = iota + 1
	b
	c
)

func main() {
	fmt.Println(a, b, c)
}

此时运行结果变成了【3 4 5】,这里 Golang 很贴心的把空行去掉了,所以此时 a = iota + 1 变成了这个定义段的第 3 行,iota 值为 2,所以 a 就是 3 了。

所以,到这里大家应该明白,下面这个案例该输出什么了吧。

package main

import "fmt"

const (
	a = iota
	b = iota + 4
	c = iota * 4
)

func main() {
	fmt.Println(a, b, c)
}

这里 iota 首次出现是第一行,所以 a 就是 0,到第二行,iota 变成了 1,所以 b 是 5。最后一行就不用多说了,结果是 8。所以,上面这个测试输出的结果就是【0 5 8】

常量定义

但仅仅到这里还不够,上面我们都是拿 iota 举例,大家可能无意中忽视了一个问题,我们凭什么就能这样用呢:

const (
	a = iota
	b
	c
)

这样 Go 就会认为 b 也是 iota?什么原理?

事实上因为 iota 方便的语法糖,我们这样的写法基本上都会伴随着 iota。但大家必须明白,为什么我们可以这么做。

其实,你还可以这么写,编译是不会报错的:

const (
	a = 1
	b
	c
)

可以试验一下,打印出来,这里 a,b,c 的值都是 1。

Get 到了么?

这其实是 Golang 隐含的能力:

In a parenthesized const declaration expressions can be implicitly repeated—this indicates a repetition of the preceding expression.

const块的连续声明如果就只有一个名字的话会把上一行内容抄下来。

事实上,你加个空行也不影响

package main

import "fmt"

const (
	a = 1

	b
	c
)

func main() {
	fmt.Println(a, b, c)
}

此时依然会输出:1 1 1

在 Golang 的官方 spec 中对此进行了说明:

Within a parenthesized const declaration list the expression list may be omitted from any but the first ConstSpec. Such an empty list is equivalent to the textual substitution of the first preceding non-empty expression list and its type if any. Omitting the list of expressions is therefore equivalent to repeating the previous list.

留空,等价于前面重复第一个非空的表达式

The number of identifiers must be equal to the number of expressions in the previous list. Together with the iota constant generator this mechanism permits light-weight declaration of sequential values:

注意表达式的数量必须和声明的变量匹配,这样的隐式约定,能够帮助我们更好的使用 iota 的变量生成能力。