iota
Go语言的iota常用于const表达式中,其值是从0开始的,const声明块中每增加一行,iota值都会自增1。
使用iota可以简化常量的定义,但其规则必须牢记,否则在阅读源码时可能会造成误解或者障碍。本章我们尝试从编译器的角度来理解iota,从而加深认识。
特性速览
在常量声明语句中,iota经常用于声明连续的整型常量。iota的取值与其出现的位置强相关。
- iota在const关键字出现时被重置为0
- const声明块中每新增一行,iota的值自增1
这种描述本身没有错误,在简单的语句中套用这种规则可以快速的计算iota的值,但在面对复杂的语句时这种规则往往充满了歧义。实际上从编译器的角度看iota,其取值的规则只有一条:
- iota代表了const声明块的行索引(下标从0开始)
这样子理解更贴近编译器的实现逻辑,也更准确。除此之外,const声明还有一个特点,即如果为常量指定了一个表达式,但后续的常量没有表达式,则继承上面的表达式。
接下来,根据这个规则来分析一个复杂的常量声明:
const (
bit0, mask0 = 1 << iota, 1<<iota - 1 //const 声明第0行,即 iota==0
bit1, mask1 //const 声明第1行,即iota==1,表达式继承上面的语句
_, _ // const 声明第2行,即iota==2
bit3, mask3 //const 声明第3行,即iota==3
)
- 第0行的表达式展开即 bit0 , mask0 = 1<<0 ,1<<0-1 , 所以bit0=1,mask0=0
- 第1行的表达式继承上一行,即 bit0 , mask0 = 1<<1 ,1<<1-1 , 所以bit0=2 , mask0=1
- 第2行没有定义常量
- 第3行没有指定表达式,继承第1行,即 bit0 , mask0 = 1<<3 ,1<<3-1 , 所以bit0=8 , mask0=7
实现原理
iota标识符仅能用于常量声明语句中,它的取值与常量声明块中的代码行数强相关,可以说它标识的正是常量声明语句中的行数(从0开始)。那么它为什么会表现出这样子的行为呢?
答案在于编译器处理常量声明语句的方式,
在编译器代码中,每个常量声明语句使用ValueSpec结构表示:
ValueSpec struct {
Doc *CommentGroup //associated documentation ; or nil
Names []*Ident //value names (len(Names) > 0)
Type Expr //value type ; or nil
Values []Expr //initial values ; or nil
Comment *CommentGroup //line comments ; or nil
}
ValueSpec 结构不仅可以用来表示常量声明,还可以表示变量声明,不过它仅表示一行声明语句,比如:
const (
//常量的注释(文档)
a , b = iota ,iota //常量的行注释
)
上面的常量声明块中仅包括一行声明语句,该语句对应一个ValueSpec结构。
- Doc表示块注释,往往会出现在文档的注释中
- Name表示常量的名字,使用切片表示单行语句中声明的多个常量
- Type 为常量类型
- Value为常量值,与Name对应,表示常量的初始值
- Comment表示行注释
编译器在构造常量时,实际上会编译ValueSpec结构中的Names切片来逐个生成常量。相关代码比较复杂,下面给出构造常量的伪算法,从中可以看出iota的作用。
通常const语句块中包含多行常量声明,那么就会对应多个ValueSpec结构,我们使用ValueSpecs表示多个ValueSpec结构,编译器构造常量的过程如下:
for iota,spec := range ValueSpecs {
for i,name := range spec.Names {
obj := NewConst(name,iota...)
...
}
}
由上面的代码可以看出iota的本质,它仅代表常量声明的索引,所以它会表现出以下特征:
- 单个const声明块中从0开始取值
- 单个const声明块中,每增加一行声明,iota的取值增1,即便声明中没有使用iota也是如此
- 单行声明语句中,即便出现多个iota,iota的取值也会保持不变