Go iota实现原理
in Go with 0 comment

Go iota实现原理

in Go with 0 comment

编译器基础知识

引用2020 Gopher China 史斌大佬的一张图

image-20210520003438552

简化一下,当前文章只关注语法分析和类型检查即可

注:当前文章go版本 master 690a8c3fb1 ,应该是go1.17左右的版本。

image-20210520004023873

开始探究iota

参考杨文大佬的文章带你开始探究 Go 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. It can be used to construct a set of related constants:

常量声明中,iota表示连续无类型整数常量,它的值是从零开始的索引,可以用来构造一组相关的常量:

先看一个简单的例子

package main

import "fmt"

const (
	one   = iota
	two   = iota
	three = iota
)

func main() {
	fmt.Println(one, two, three)
}

// Output:
// 0 1 2

先从这个例子入手,后续再分析复杂的场景

Note:例子中的one、two、three实际值为0、1、2,后续流程分析完才发现这个问题,不修改例子了。

先打印AST看一下,做一下大概的了解。

package main

import (
	"go/ast"
	"go/parser"
	"go/token"
	"log"
	"path/filepath"
)

func main() {
	fset := token.NewFileSet()
	// 这里取绝对路径,方便打印出来的语法树可以转跳到编辑器
	path, _ := filepath.Abs("XXX/main.go")
	f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
	if err != nil {
		log.Println(err)
		return
	}
	// 打印语法树
	ast.Print(fset, f)
}
0  *ast.File {
1  .  Package: XXX/main.go:1:1
2  .  Name: *ast.Ident {
3  .  .  NamePos: XXX/main.go:1:9
4  .  .  Name: "main"
5  .  }
6  .  Decls: []ast.Decl (len = 3) { // 共有3个Top Level Decl
7  .  .  0: *ast.GenDecl { // import "fmt"
8  .  .  .  TokPos: XXX/main.go:3:1
9  .  .  .  Tok: import
10  .  .  .  Lparen: -
11  .  .  .  Specs: []ast.Spec (len = 1) {
12  .  .  .  .  0: *ast.ImportSpec {
13  .  .  .  .  .  Path: *ast.BasicLit {
14  .  .  .  .  .  .  ValuePos: XXX/main.go:3:8
15  .  .  .  .  .  .  Kind: STRING
16  .  .  .  .  .  .  Value: "\"fmt\""
17  .  .  .  .  .  }
18  .  .  .  .  .  EndPos: -
19  .  .  .  .  }
20  .  .  .  }
21  .  .  .  Rparen: -
22  .  .  }
23  .  .  1: *ast.GenDecl {  // const ( XXX )
24  .  .  .  TokPos: XXX/main.go:5:1
25  .  .  .  Tok: const
26  .  .  .  Lparen: XXX/main.go:5:7
27  .  .  .  Specs: []ast.Spec (len = 3) { // 共声明了3个常量
28  .  .  .  .  0: *ast.ValueSpec {
29  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
30  .  .  .  .  .  .  0: *ast.Ident {
31  .  .  .  .  .  .  .  NamePos: XXX/main.go:6:2
32  .  .  .  .  .  .  .  Name: "one"  // 常量名字
33  .  .  .  .  .  .  .  Obj: *ast.Object {
34  .  .  .  .  .  .  .  .  Kind: const
35  .  .  .  .  .  .  .  .  Name: "one"
36  .  .  .  .  .  .  .  .  Decl: *(obj @ 28)
37  .  .  .  .  .  .  .  .  Data: 0  // “只是iota的值”,后续复杂场景可以理解这句话的意思
38  .  .  .  .  .  .  .  }
39  .  .  .  .  .  .  }
40  .  .  .  .  .  }
41  .  .  .  .  .  Values: []ast.Expr (len = 1) {
42  .  .  .  .  .  .  0: *ast.Ident {
43  .  .  .  .  .  .  .  NamePos: XXX/main.go:6:10
44  .  .  .  .  .  .  .  Name: "iota"  // 常量对应的值类型为iota
45  .  .  .  .  .  .  }
46  .  .  .  .  .  }
47  .  .  .  .  }
48  .  .  .  .  1: *ast.ValueSpec { //省略部分字段
49  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
50  .  .  .  .  .  .  0: *ast.Ident {
52  .  .  .  .  .  .  .  Name: "two"
53  .  .  .  .  .  .  .  Obj: *ast.Object {
57  .  .  .  .  .  .  .  .  Data: 1
58  .  .  .  .  .  .  .  }
59  .  .  .  .  .  .  }
60  .  .  .  .  .  }
61  .  .  .  .  .  Values: []ast.Expr (len = 1) {
62  .  .  .  .  .  .  0: *ast.Ident {
64  .  .  .  .  .  .  .  Name: "iota"
65  .  .  .  .  .  .  }
66  .  .  .  .  .  }
67  .  .  .  .  }
68  .  .  .  .  2: *ast.ValueSpec {
69  .  .  .  .  .  Names: []*ast.Ident (len = 1) {
70  .  .  .  .  .  .  0: *ast.Ident {
72  .  .  .  .  .  .  .  Name: "three"
73  .  .  .  .  .  .  .  Obj: *ast.Object {
77  .  .  .  .  .  .  .  .  Data: 2
78  .  .  .  .  .  .  .  }
79  .  .  .  .  .  .  }
80  .  .  .  .  .  }
81  .  .  .  .  .  Values: []ast.Expr (len = 1) {
82  .  .  .  .  .  .  0: *ast.Ident {
84  .  .  .  .  .  .  .  Name: "iota"
85  .  .  .  .  .  .  }
86  .  .  .  .  .  }
87  .  .  .  .  }
88  .  .  .  }
89  .  .  .  Rparen: XXX/main.go:9:1
90  .  .  }
91  .  .  2: *ast.FuncDecl { // func main
92  .  .  .  Name: *ast.Ident {
93  .  .  .  .  NamePos: XXX/main.go:11:6
94  .  .  .  .  Name: "main"
95  .  .  .  .  Obj: *ast.Object {
96  .  .  .  .  .  Kind: func
97  .  .  .  .  .  Name: "main"
98  .  .  .  .  .  Decl: *(obj @ 91)
99  .  .  .  .  }
100  .  .  .  }
101  .  .  .  
148  .  .  }
149  .  }
150  .  Scope: *ast.Scope {
151  .  .  Objects: map[string]*ast.Object (len = 4) {
152  .  .  .  "three": *(obj @ 73)
153  .  .  .  "main": *(obj @ 95)
154  .  .  .  "one": *(obj @ 33)
155  .  .  .  "two": *(obj @ 53)
156  .  .  }
157  .  }
158  .  Imports: []*ast.ImportSpec (len = 1) {
159  .  .  0: *(obj @ 12)
160  .  }
161  .  Unresolved: []*ast.Ident (len = 4) {
162  .  .  0: *(obj @ 42)
163  .  .  1: *(obj @ 62)
164  .  .  2: *(obj @ 82)
165  .  .  3: *(obj @ 114)
166  .  }
167  }

源码剖析

调试Go编译器代码

使用Goland调试Go编译器代码

$GOPATH/src/cmd/compile/main.go

image-20210522095340794

iota值是如何确定的

关注ConstDecl的处理

先在$GOPATH/src/cmd/compile/internal/noder/noder.go:325打断点观察一下

1621648930041

上方说了,当前文件“共有3个Top Level Decl”,为什么这里的decls有5个?

实际上是把 Top Level Decl 中的const(XXX) 展平了

这里关注一个新的概念:Group,从调试信息中可以看出,onetwothree这三个ConstDecl指向了同一个Group

WX20210522-101413@2x

前面埋了两个坑 csGroup

这三个ConstDecl会按顺序执行这一行代码,并传入cs

l = append(l, p.constDecl(decl, &cs)...)

继续来看p.constDecl的实现(删除部分源码,仅关注当前场景的逻辑代码,后面会再解析剩余源码)

当前场景下,namesvalues的长度都为1

type constState struct {
	group  *syntax.Group
	typ    ir.Ntype
	values []ir.Node
	iota   int64         // 初始值为0
}

func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
	// 当Group为nil 或者 当前处理的 Group 与 cs 缓存的 Group 不相同时,新建一个 cs
	// Group,顾名思义,“组”
	// 当前场景中 ,`one`、`two`、`three` 被同一个 const ()包裹,属于同一个组
	// cs是在外层循环开始时定义的,初始化的 cs 是一个空值,所以不属于任何Group
	// cs是通过指针传递的,所以在该函数中新建cs,外层是可以感知到的cs的变化的,并会传递给下一个ConstDecl的p.constDecl方法
	// 所以在当前场景中,在处理const one时,会进入该分支,更新Group信息
	// 在处理two、three时,由于归属同一Group,不会再进入该分支
	// 通过这种方法达到了同一个const Group中iota值自加的目的
	if decl.Group == nil || decl.Group != cs.group {
		*cs = constState{
			group: decl.Group,
		}
	}

	names := p.declNames(ir.OLITERAL, decl.NameList)
	typ := p.typeExprOrNil(decl.Type)

	var values []ir.Node
	if decl.Values != nil {
		values = p.exprList(decl.Values)
	}

	nn := make([]ir.Node, 0, len(names))
	for i, n := range names {
		if i >= len(values) {
			base.Errorf("missing value in const declaration")
			break
		}
		v := values[i]
		typecheck.Declare(n, typecheck.DeclContext)

		n.Ntype = typ
		n.Defn = v
		// 在处理one时,iota为初始值0
		n.SetIota(cs.iota) // 设置这个Node的iota值

		nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))
	}

	if len(values) > len(names) {
		base.Errorf("extra expression in const declaration")
	}

	cs.iota++ // iota值自增,所以在后续继续处理two、three时,对应的值已经更改为1,2

	return nn
}

SetIota实际上就是更改了Node中Offset_字段的值,虽然这个Offset有些词不达意,其实对于不同的Decl类型,Offset字段的含义是不一样的,复用该字段,来节省空间。

func (n *Name) SetIota(x int64)        { n.Offset_ = x }

继续查看一下返回值nn中的信息

可以简单认为,将该Decl节点换了一种形式,我们关注的信息都还在,这个符号的名字是one,值是iotaiota的真实值Offset为0。

另外注意下,Defn中的sym字段,该字段是个指针,它的Def后续会用来标识和计算iota的值,当前还为空,记住,Defn中的sym字段是个指针,后续还有个骚操作。

WX20210522-204743@2x

再继续看一下func (p *noder) decls函数的返回值

这里这有4个Node,是因为对于syntax.ImportDecl,没有append加入到返回值中。

第一个Node上方已观察过,这里只关注第二个和第三个Node的信息,最后一个Node为func main,暂时也不关注。

WX20210522-210025@2x

iota值的读取

继续执行编译流程,关注一下DeclareUniverse函数,在这个函数中会将上方说到的Defn->sym->Def字段统一赋值。

func DeclareUniverse() {
	// Operationally, this is similar to a dot import of builtinpkg, except
	// that we silently skip symbols that are already declared in the
	// package block rather than emitting a redeclared symbol error.

	for _, s := range types.BuiltinPkg.Syms {
		if s.Def == nil {
			continue
		}
		s1 := Lookup(s.Name)
		if s1.Def != nil {
			continue
		}

		s1.Def = s.Def
		s1.Block = s.Block
	}
}

这里其实对于Go中的所有内建(builtin)类型进行了处理,看下Go的内建类型都有哪些,这里应该和$GOPATH/src/builtin/builtin.go是可以对应的。

WX20210522-210725@2x

Defn->sym->Def赋值后的信息如下,名字仍为iota,类型为OIOTA,后续会根据该信息进行进一步处理。

image-20210522211512724

func typecheckdef(n *ir.Name)中拿到Defn进行进一步操作,注意,这里取出后就设置为nil清空了。

e := n.Defn
n.Defn = nil
if e == nil {
  ir.Dump("typecheckdef nil defn", n)
  base.ErrorfAt(n.Pos(), "xxx")
}

e = Expr(e)

接着到达func Resolve(n ir.Node) (res ir.Node)函数

该函数会对OIOTA类型的节点,取出其对应的真实值,并返回一个Int节点,进行后续的操作。

WX20210522-212737@2x

iota的值取出的正常流程也比较简单从Offest字段直接获取即可,另外一个if分支我当前还不清楚是什么场景,先留个坑。

func getIotaValue() int64 {
	if i := len(typecheckdefstack); i > 0 {
		if x := typecheckdefstack[i-1]; x.Op() == ir.OLITERAL {
			// 当前场景走这个分支
			return x.Iota()
		}
	}

	if ir.CurFunc != nil && ir.CurFunc.Iota >= 0 {
		return ir.CurFunc.Iota
	}

	return -1
}

func (n *Name) Iota() int64            { return n.Offset_ }

至此,iota类型的生成,变换,读取的逻辑已经都找到了,iota这个语法糖也已经变为正常的int值,可以进行后续的正常处理了。

再来回顾一下iota值计算前后的Decls的状态

处理前:

const two和three的val为空。

image-20210522213911917

处理后:

image-20210522214411041

Group的生成和划分

还有一个坑要填,上方说的Group是如何生成和划分的?

这个其实是在Parser阶段就已经划分好了

找到 const 关键字时的处理

WX20210522-215112@2x

继续处理的逻辑,其实比较简单

WX20210522-215226@2x

// ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .
func (p *parser) constDecl(group *Group) Decl {
	if trace {
		defer p.trace("constDecl")()
	}

	d := new(ConstDecl)
	d.pos = p.pos()
	d.Group = group
	d.Pragma = p.takePragma()

	d.NameList = p.nameList(p.name())
	if p.tok != _EOF && p.tok != _Semi && p.tok != _Rparen {
		d.Type = p.typeOrNil()
		if p.gotAssign() {
			d.Values = p.exprList()
		}
	}

	return d
}

当前token位置在one的位置,是名字信息,调用p.name()后,token会向后走,在gotAssign中取到=p.exprList()中取到value

image-20210522220701827

解析完成后的结果为,根据地址可以看出,归属于同一个Group

1621693040112

后续的内容就可以和上方的衔接上了。

几个额外的场景

不显式写出iota

package main

import "fmt"

const (
	zero = iota
	one
	two
)

func main() {
	fmt.Println(zero, one, two)
}

// Output:
// 0 1 2

观察一下func (p *noder) declsdecls中的情况(计算iota值之前)

只有zero的Value是iota,one、two的Value都为空,且这三个常量属于同一个Group

WX20210522-232226@2x

再次解析一下constDecl函数中相关的逻辑代码

Note:处理zeroonetwo的过程中会进入该函数三次

func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
	// 处理zero时,新建一个constState
	if decl.Group == nil || decl.Group != cs.group {
		*cs = constState{
			group: decl.Group,
		}
	}

	names := p.declNames(ir.OLITERAL, decl.NameList)
	typ := p.typeExprOrNil(decl.Type)

	var values []ir.Node
	if decl.Values != nil {
		// 处理zero时,Values不为空,将zero的typ和values信息缓存到cs中
		values = p.exprList(decl.Values)
		cs.typ, cs.values = typ, values
	} else {
		// 处理two、three时.Values为空
		// 读取cs中缓存的typ和values信息(即与zero的typ和values信息相同)
		// 达到”继承“上一个显式所写的 iota 表达式的目的
		if typ != nil {
			base.Errorf("const declaration cannot have type without expression")
		}
		typ, values = cs.typ, cs.values
	}

	cs.iota++

	return nn
}

const值是和iota相关的表达式

package main

import "fmt"

const (
	test1 = iota + 2
	test2
)

func main() {
	fmt.Println(test1, test2)
}

// Output:
// 2 3

在AST中存储的结构为:

WX20210522-223606@2x

与上方所述流程不同的是,在将iota替换为对应的值后,还有一个二元表达式加法的计算和替换过程

func EvalConst(n ir.Node) ir.Node {
	// Pick off just the opcodes that can be constant evaluated.
	switch n.Op() { //
	case ir.OADD, ir.OSUB, ir.OMUL, ir.ODIV, ir.OMOD, ir.OOR, ir.OXOR, ir.OAND, ir.OANDNOT:
		n := n.(*ir.BinaryExpr)
		nl, nr := n.X, n.Y // 获取二元表达式的两个操作数
		if nl.Op() == ir.OLITERAL && nr.Op() == ir.OLITERAL {
			rval := nr.Val()

			// check for divisor underflow in complex division (see issue 20227)
			if n.Op() == ir.ODIV && n.Type().IsComplex() && constant.Sign(square(constant.Real(rval))) == 0 && constant.Sign(square(constant.Imag(rval))) == 0 {
				base.Errorf("complex division by zero")
				n.SetType(nil)
				return n
			}
			if (n.Op() == ir.ODIV || n.Op() == ir.OMOD) && constant.Sign(rval) == 0 {
				base.Errorf("division by zero")
				n.SetType(nil)
				return n
			}

			tok := tokenForOp[n.Op()]
			if n.Op() == ir.ODIV && n.Type().IsInteger() {
				tok = token.QUO_ASSIGN // integer division
			}
			return OrigConst(n, constant.BinaryOp(nl.Val(), tok, rval))
		}
	return n
}
  
// OrigConst returns an OLITERAL with orig n and value v.
func OrigConst(n ir.Node, v constant.Value) ir.Node {
	lno := ir.SetPos(n)
	v = convertVal(v, n.Type(), false)
	base.Pos = lno

	switch v.Kind() {
	case constant.Int:
		if constant.BitLen(v) <= ir.ConstPrec {
			break
		}
		fallthrough
	case constant.Unknown:
		what := overflowNames[n.Op()]
		if what == "" {
			base.Fatalf("unexpected overflow: %v", n.Op())
		}
		base.ErrorfAt(n.Pos(), "constant %v overflow", what)
		n.SetType(nil)
		return n
	}
	// 返回一个新的ConstExpr Node
	return ir.NewConstExpr(v, n)
}
  
  
func BinaryOp(x_ Value, op token.Token, y_ Value) Value {
	x, y := match(x_, y_)

	switch x := x.(type) {
	case unknownVal:
		return x
	case int64Val:
		a := int64(x)
		b := int64(y.(int64Val))
		var c int64
		switch op {
		case token.ADD:
			c = a + b // 计算Add之后的值
		}
		return int64Val(c)
	}
}

同一行中有多个常量

package main

import "fmt"

const (
	test1, test2 = iota + 1, iota + 2
	test3, test4 = iota + 3, iota + 4
)

func main() {
	fmt.Println(test1, test2, test3, test4)
}

// Output:
// 1 2 4 5

实际上 test1、test2属于同一个Decl、test3、test4属于同一个Decl,当然它们都属于同一个Group。

上方的例子中,NameListValues中都是只有一个值,实际上它是可以存储多个值的。

WX20210522-233851@2x

再次回到constDecl函数中相关的逻辑代码

这个例子中会进入该函数两次,第一次时NameList[test1,test2] ,第二次时NameList[test3,test4]

func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []ir.Node {
	if decl.Group == nil || decl.Group != cs.group {
		*cs = constState{
			// cs.iota 初始值为0
			group: decl.Group,
		}
	}

	names := p.declNames(ir.OLITERAL, decl.NameList)
	typ := p.typeExprOrNil(decl.Type)

	var values []ir.Node

	nn := make([]ir.Node, 0, len(names))
	// 遍历NameList中的所有值
	for i, n := range names {
		if i >= len(values) {
			base.Errorf("missing value in const declaration")
			break
		}
		v := values[i]
		if decl.Values == nil {
			v = ir.DeepCopy(n.Pos(), v)
		}
		typecheck.Declare(n, typecheck.DeclContext)

		n.Ntype = typ
		n.Defn = v
		n.SetIota(cs.iota) // 将其都设置为cs.iota的当前值

		nn = append(nn, ir.NewDecl(p.pos(decl), ir.ODCLCONST, n))
	}
	// 更新cs.iota
	cs.iota++

	return nn
}

计算iota后,对应的值如下

image-20210522235219155

image-20210522235329702

const组中存在_的情况

package main

import "fmt"

const (
	zero = iota
	one
	two
	three
	_
	five = iota
)

func main() {
	fmt.Println(zero, one, two, three, five)
}

// Output:
// 0 1 2 3 5

分析过程与上述相似,读者可自行分析。

const组中第一行不是iota

package main

import "fmt"

const (
	five  = 5
	two   = iota
	three = iota
)

func main() {
	fmt.Println(five, two, three)
}

// Output:
// 5 1 2

分析过程与上述相似,读者可自行分析。

总结

分析过iota的源码之后,如何人工debug iota的值呢?