本文中引用的源码均标注了 Golang 源码仓库链接,branch 为
release-branch.go1.21
(本文在编写时 Go 1.21 还未正式发布,正式版可能会有少量变化)。
init()
在不规范使用情况下产生的现象
在同一个 go 文件里,初始相关操作的执行顺序是 const
-> var
-> init()
。显然,如果同一个文件里有多个 init()
,那么将按照声明顺序来执行。
如果 Package 的 init()
分布在不同的文件里,将会按照什么顺序来执行呢?
有如下场景:
1main.go
2a/b.go
3a/c.go
1// main.go
2package main
3
4import "go-init/a"
5
6func main() {
7 a.A()
8}
1// a/b.go
2package a
3
4func init() {
5 println("b init")
6}
7
8func A() int {
9 println("A")
10 return 0
11}
1// a/c.go
2package a
3
4func init() {
5 println("c init")
6}
执行 go run main.go
,得到输出:
1b init
2c init
3A
接下来将 a/b.go
改名为 a/d.go
,再次执行 go run main.go
,输出:
1c init
2b init
3A
可以看到有 [现象]:a/b.go
和 a/c.go
的 init()
函数的执行顺序是按照文件名的字母顺序来的,将 a/b.go
改名后,其文件名顺序排在了 a/c.go
之后,最终 init()
执行也排在了之后。
还有更多复杂的情况,例如:
- 如果 import 的包之间存在依赖关系,那么这些包的
init()
的执行顺序是怎样的? - 如果 Package 的
init()
分布在不同的文件里,而且这些文件里有交叉依赖的var
全局变量,那么init()
和这些全局变量初始化的执行顺序又是怎样的?
实际上,要真正弄清楚这些,需要深入 Go 编译器,从根源弄清原理。init()
的处理是 Go 编译过程中的重要一环。
编译的起点 gc.Main()
Golang 编译器相关源码位于 go/src/cmd/compile/
。
Go 编译处理的单位是 Package,得到的结果是 Object 文件。在一次编译过程开始时会读取 Package 中所有文件内容进行词法和语法分析。我们很容易就能找到编译器的入口文件 main.go
:
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/main.go#L45
2func main() {
3 // disable timestamps for reproducible output
4 log.SetFlags(0)
5 log.SetPrefix("compile: ")
6
7 buildcfg.Check()
8 archInit, ok := archInits[buildcfg.GOARCH]
9 if !ok {
10 fmt.Fprintf(os.Stderr, "compile: unknown architecture %q\n", buildcfg.GOARCH)
11 os.Exit(2)
12 }
13
14 gc.Main(archInit)
15 base.Exit(0)
16}
gc.Main()
完成了整个编译流程,其内容是本文的重点;编译流程本身比较清晰,但内容很多,在本文中主要关心 init()
相关的处理。
为了方便理解,请先阅读编译器部分的 README.md,了解编译器的基本流程和相关概念;下面也简单介绍一下编译器的流程,补充一些细节,便于理解为什么 Go 编译器现在是这样一个结构。
编译流程
1. Parsing
词法和语法分析得到 AST(Abstract Syntax Tree,抽象语法树),每个 AST 等价于一个源文件。关于树的结构和节点的定义见 internal/syntax/nodes.go,Go 源码中的所有元素都能在这里找到对应的结构,极其基础和重要。
例如:
1// X[Index[0] : Index[1] : Index[2]]
2SliceExpr struct {
3 X Expr
4 Index [3]Expr
5 // Full indicates whether this is a simple or full slice expression.
6 // In a valid AST, this is equivalent to Index[2] != nil.
7 // TODO(mdempsky): This is only needed to report the "3-index
8 // slice of string" error when Index[2] is missing.
9 Full bool
10 expr
11}
我们可以从中知道 slice[:] 操作实际上可以有三个参数,分别表示指针(新起始位置)、长度和容量。
同时也可以看到 Comments 相关结构仍在开发之中,后续可能会加入 AST 用于生成更加结构化的文档。
2. Type checking
类型检查,types2
是从 go/types
移植而来的,在这里需要结合发展历史来理解。
在一开始,Go 的编译器是使用 C 来实现的。到 Go 1.5 版本,实现了自举,其中编译过程中类型检查使用的是 Go 实现的传统检查算法,位于 internal/gc/typecheck.go
;同时 Go 1.5 版本在标准库里面加入了 go/types
,便于开发者开发 Go 代码分析工具。随着各种代码检查工具的涌现,go/types
发展迅猛,相比较而言,internal/gc/typecheck.go
涉及编译器过于底层,发展较慢。
直到 Go 1.17 开始开发,需要将泛型作为实验特性加入,此时编译器的类型检查已经无法满足要求,好在 go/types
已经十分成熟,借助其强大的类型推导能力,在编译器中实现了对泛型的处理;这也是 internal/types2
和 go/types
一开始相同的原因。后续对 go/types
问题的修复也应同步到 types2
,这样才能保证编译器和代码分析工具的一致性,同时也相当于让更多人参与了编译器的改进;当然,编译器自身也有些特殊需求需要在 types2
中实现。由于现在有两种并行的实现,因此 internal/gc/typecheck.go
被抽取出来,成为了 internal/typecheck
包。
在 Go 1.5 之前,编译器使用 C 实现,不存在 Go 实现的类型检查。
在 Go 1.5 - 1.16,编译器使用 Go 实现,类型检查使用
gc/typecheck.go
,官方提供了go/types
包。在 Go 1.17 时泛型还只是可选项,因此编译器提供了参数
-G
来选择是否开启泛型,实际上,当G=0
时编译器会使用旧的typecheck
来进行类型检查;当G=3
时使用types2
(go/types
移植而来)进行类型检查以支持泛型。在 Go 1.18 正式推出泛型以后,
-G
参数仍然存在,只不过默认值改成了G=3
,也就是说,现在编译器默认使用types2
进行类型检查。在 Go 1.19 推出后,
-G
参数被移除,编译器只能使用types2
进行类型检查。
例如 Commit: 8fd2875:
修改 src/go/types
后,也同步修改了 src/cmd/compile/internal/types2
下的内容。
3. IR construction(“noding”)
IR(Intermediate Representation,中间表示)是一种介于 AST 和汇编代码之间的表示,是一种更加抽象的表示,能够更好地表示语义。这一步就是将 AST 转换为 IR,这个过程称作 noding
。
在 Go 1.17 之前,并没有 IR 的概念,或者说有,但是还不叫 IR。
在 Go 1.17,当
G=0
时,编译器可选择使用internal/typecheck
进行类型检查,因此对应使用noder
进行noding
;当G=3
时,编译器使用types2
进行类型检查,因此使用相应的新实现来进行noding
, 称之为noder2
。在 Go 1.18,同样可以通过
-G
参数来选择使用internal/typecheck
或者types2
进行类型检查,因此noder
和noder2
仍然是并存的。在 Go 1.19 之后,编译器只能使用
types2
进行类型检查,因此noder2
也是唯一的noding
实现。
其实 IR 也是一种形式的 AST,被称为 GC AST
(Go Compiler AST)。那么为什么要转换呢?主要原因是自 Go 1.5 实现自举时,参考旧 C 的实现来完成了 AST 上的类型检查等等后续操作;但是新的 Go 实现的词法语法分析得到的 AST 只是分别与源文件对应,还未处理 import 以及合并,并不完整;好在这个转换并不复杂。
旧的处理流程如下所示:
1// 旧处理流程 Go 1.5 - 1.16
2[AST1,AST2,...] := Parse([file1,file2,...])
3
4// 处理 import,合并 AST
5IR := Noder([AST1,AST2,...])
6
7// 类型检查
8Typecheck(IR)
9MiddleEndOP(IR)
10SSA := SSAGen(IR)
11MACHINE_CODE := CodeGen(SSA)
在 Go 1.17 引入 types2
后,由于 types2
是作用于 AST 上的,因此新的处理流程变成了:
1// 新处理流程 Go 1.17
2[AST1,AST2,...] := Parse([file1,file2,...])
3
4#if G=3
5 // 处理 import,类型检查,处理泛型
6 TypeInfo := Types2([AST1,AST2,...])
7 IR := Noder2([AST1,AST2,...],TypeInfo)
8#elseif G=0
9 // 处理 import,合并 AST
10 IR := Noder([AST1,AST2,...])
11#endif
12
13// 之后完全一致
14Typecheck(IR)
15MiddleEndOp(IR)
16SSA := SSAGen(IR)
17MACHINE_CODE := CodeGen(SSA)
noder2
的实现位于 internal/ir
。会发现当 G=3
时,虽然用了 internal/types2
来进行类型检查,但是后续在 IR 上还是跑了一遍 internal/typecheck
,在这里有许多原因,主要是 internal/typecheck
会对 IR 进行一些修改调整,因此还需要保留,详情可以看这里的注释:internal/noder/unified.go#L51。
在 Go 1.18 又引入了 Unified IR(GOEXPERIMENT=unified 开启),于是乎三种流程并行存在:
1// 新处理流程 Go 1.18
2[AST1,AST2,...] := Parse([file1,file2,...])
3
4#if Unified
5 IR := Unified([AST1,AST2,...])
6#else
7 #if G=3
8 // 处理 import,类型检查,处理泛型
9 TypeInfo := Types2([AST1,AST2,...])
10 IR := Noder2([AST1,AST2,...],TypeInfo)
11 #elseif G=0
12 // 处理 import,合并 AST
13 IR := Noder([AST1,AST2,...])
14 #endif
15 Typecheck(IR)
16#endif
17
18MiddleEndOp(IR)
19SSA := SSAGen(IR)
20MACHINE_CODE := CodeGen(SSA)
在 Go 1.19 移除了 G=0
的流程:
1// 新处理流程 Go 1.19
2[AST1,AST2,...] := Parse([file1,file2,...])
3
4#if Unified
5 IR := Unified([AST1,AST2,...])
6#else
7 // 处理 import,类型检查,处理泛型
8 TypeInfo := Types2([AST1,AST2,...])
9 IR := Noder2([AST1,AST2,...],TypeInfo)
10 Typecheck(IR)
11#endif
12
13MiddleEndOp(IR)
14SSA := SSAGen(IR)
15MACHINE_CODE := CodeGen(SSA)
在 Go 1.21 正式启用了 Unified IR,因此 unified
也就是唯一的 noding
实现了,确实实现了统一,欢迎来到 Go 1.21 !(实际上需要处理的东西其实没有改变,只是整合在了 Unified
内,因此原来的包还依然存在)
1// 新处理流程 Go 1.21
2[AST1,AST2,...] := Parse([file1,file2,...])
3
4IR := Unified([AST1,AST2,...])
5
6MiddleEndOp(IR)
7SSA := SSAGen(IR)
8MACHINE_CODE := CodeGen(SSA)
下面是各种 noder
在不同版本的存在状态:
- Go 1.17 之前:noder
- Go 1.17: noder, noder2
- Go 1.18: noder, noder2, unified
- Go 1.19: noder2, unified
- Go 1.20: noder2, unified
- Go 1.21: unified
4. Middle end
internal/deadcode
(dead code elimination)internal/inline
(function call inlining)internal/devirtualize
(devirtualization of known interface method calls)internal/escape
(escape analysis)
5. Walk,SSA Gen 以及机器码生成
- Walk 遍历 IR,拆分复杂的语句以及将语法糖转换成基础的语句
- SSA Gen 将 IR 转化为 Static Single Assignment (SSA) 形式,此时还与具体的机器无关
- 机器码生成会根据架构以及更多机器相关的信息,对 SSA 进行优化;同时进行栈帧分配,寄存器分配,指针存活分析等等,最终经过汇编器
cmd/internal/obj
生成机器码。
流程中 init 相关的处理
前面我们了解了 Go 编译器的流程,以及其发展变化的历史。接下来我们来看看其中 init 相关的具体处理。
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/gc/main.go#L59
2// Main parses flags and Go source files specified in the command-line
3// arguments, type-checks the parsed Go package, compiles functions to machine
4// code, and finally writes the compiled package definition to disk.
5func Main(archInit func(*ssagen.ArchInfo)) {
6 ...
7 // Parse and typecheck input.
8 noder.LoadPackage(flag.Args())
9 ...
10 // Create "init" function for package-scope variable initialization
11 // statements, if any.
12 //
13 // Note: This needs to happen early, before any optimizations. The
14 // Go spec defines a precise order than initialization should be
15 // carried out in, and even mundane optimizations like dead code
16 // removal can skew the results (e.g., #43444).
17 pkginit.MakeInit()
18 ...
19 // Build init task, if needed.
20 if initTask := pkginit.Task(); initTask != nil {
21 typecheck.Export(initTask)
22 }
23 ...
gc.Main()
流程中主要有以上三部分对 init 进行了处理,接下来我们分别看看这三部分。
noder.LoadPackage()
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/noder/noder.go#L27
2func LoadPackage(filenames []string) {
3 ...
4 noders := make([]*noder, len(filenames))
5 ...
6 go func() {
7 for i, filename := range filenames {
8 ...
9 go func() {
10 ...
11 f, err := os.Open(filename)
12 ...
13 p.file, _ = syntax.Parse(fbase, f, p.error, p.pragma, syntax.CheckBranches) // errors are tracked via p.error
14 }()
15 }
16 }()
17 ...
18 unified(m, noders)
19}
可以看到 LoadPackage()
会并行的对每个文件进行读取以及词法语法分析,构建 AST。并将得到的 AST 列表传递给 unified()
进行统一处理。
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/noder/unified.go#L71
2func unified(m posMap, noders []*noder) {
3 ...
4 data := writePkgStub(m, noders)
5 ...
6 target := typecheck.Target
7 r := localPkgReader.newReader(pkgbits.RelocMeta, pkgbits.PrivateRootIdx, pkgbits.SyncPrivate)
8 r.pkgInit(types.LocalPkg, target)
9
10 // 后面均为 `internal/typecheck` 的处理,与 init 无关
11 // Type-check any top-level assignments. We ignore non-assignments
12 // here because other declarations are typechecked as they're
13 // constructed.
14 for i, ndecls := 0, len(target.Decls); i < ndecls; i++ {
15 switch n := target.Decls[i]; n.Op() {
16 case ir.OAS, ir.OAS2:
17 target.Decls[i] = typecheck.Stmt(n)
18 }
19 }
20
21 readBodies(target, false)
22
23 // Check that nothing snuck past typechecking.
24 for _, n := range target.Decls {
25 if n.Typecheck() == 0 {
26 base.FatalfAt(n.Pos(), "missed typecheck: %v", n)
27 }
28
29 // For functions, check that at least their first statement (if
30 // any) was typechecked too.
31 if fn, ok := n.(*ir.Func); ok && len(fn.Body) != 0 {
32 if stmt := fn.Body[0]; stmt.Typecheck() == 0 {
33 base.FatalfAt(stmt.Pos(), "missed typecheck: %v", stmt)
34 }
35 }
36 }
37 ...
38}
其中 writePkgStub()
完成了类型检查。接下来的调用链有点长,在这里就不放源代码了,大致流程如下:
writePkgStub() -> noder.checkFiles -> conf.Check() -> Checker.Files() -> check.checkFiles()
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/types2/check.go#L335
2func (check *Checker) checkFiles(files []*syntax.File) (err error) {
3 ...
4 print("== initFiles ==")
5 check.initFiles(files)
6
7 print("== collectObjects ==")
8 check.collectObjects()
9
10 print("== packageObjects ==")
11 check.packageObjects()
12
13 print("== processDelayed ==")
14 check.processDelayed(0) // incl. all functions
15
16 print("== cleanup ==")
17 check.cleanup()
18
19 print("== initOrder ==")
20 check.initOrder()
21 ...
22}
initFiles()
用于检查文件开头的 package 语句所声明的名称是否符合要求,例如要跟当前 package 名一致,否则忽略这个文件(都经过词法语法分析了,白分析了,当然编译前就能检查出这些问题,一般不会进行到这里才发现)。
collectObjects()
在此处对 import 的 Package 进行了加载,并将其置于相应的 Scope 中。可以看到这里仍然是按照文件顺序在进行处理,通过 check.impMap
来缓存已经加载的 Package;同时用 pkgImports map[*Package]bool
来记录本 Package 已经引用的 Package,避免其重复加入 pkg.imports
数组。
同时,还能从中看到一些特殊 import 的处理,例如 import . 和 import _ 以及别名。DotImport 会将 imported package 中的导出符号全部遍历导入到当前的 FileScope 中,而一般情况下是将 imported package 整个加入到当前的 FileScope 中,这样会有额外的层次结构。
注意这里提到了 FileScope,我们知道在 Go 的同一个 Package 下,许多声明是不存在 FileScope 的,例如全局变量在一个文件中声明,另一个文件中可以直接使用;同名也会发生冲突,因为这些都在同一个 PackageScope 下。但是对于 import 操作来说,每个文件都有自己需要 import 的内容,因此需要一个 FileScope 来记录区分这些信息。
Scope 结构组织好后,还需要检查 FileScope 跟 PackageScope 之间的冲突问题,这主要是 DotImport 导致的。
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/types2/resolver.go#L472
2// verify that objects in package and file scopes have different names
3for _, scope := range fileScopes {
4 for name, obj := range scope.elems {
5 if alt := pkg.scope.Lookup(name); alt != nil {
6 ...
initOrder()
是对一些有依赖关系的全局声明进行排序,并未涉及 init 的处理,例如:
1var (
2 // a depends on b and c, c depends on f
3 a = b + c
4 b = 1
5 c = f()
6
7 // circular dependency
8 d = e
9 e = d
10)
在 Go 中,能够被用于初始化表达式的对象被称为 Dependency 对象,有 Const, Var, Func 这三类。先构建对象依赖关系的有向图(Directed Graph),再以每个节点的依赖数目为权重构建最小堆(MinHeap)并以此堆作为最小优先级队列(PriorityQueue),因此队列头部的对象总是依赖其它对象最少的,所以该队列的遍历顺序就是初始化的顺序,是很常规的处理思路。要注意常量的初始化比较简单,在构建时就已经确定,在这里仍然加入是为了检测循环依赖。
1https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/noder/unified.go#L209
2func writePkgStub(m posMap, noders []*noder) string {
3 // 类型检查
4 pkg, info := checkFiles(m, noders)
5
6 pw := newPkgWriter(m, pkg, info)
7 pw.collectDecls(noders)
8 ...
9 var sb strings.Builder
10 pw.DumpTo(&sb)
11
12 // At this point, we're done with types2. Make sure the package is
13 // garbage collected.
14 freePackage(pkg)
15
16 return sb.String()
17}
最后再回到开始,可见 writePkgStub
包含了 internal/types2
的类型检查;类型检查会涉及到外部包的导出类型,也就是说会处理 import 语句;同时,类型检查的过程中也生成了一份 types2.Package
以及 types2.info
,其中 types2.package
包含 Scope 层次信息以及每个 Scope 中的 Object 信息;types2.info
包含了类型检查中生成的类型信息;最后通过 pkgWriter
将这两个信息整合序列化为字符串,也就是最终得到的 data
。
实际上,这个 data
就是 Unified IR 的导出;接下来使用 pkgReader
将 data
重新构建为 IR,存储在 typecheck.Target
。
明明步骤紧接在一起,为什么要把 Unified IR 先
export
再import
呢? 这样做主要是为了将 Unified IR 与后续部分完全解耦,可以看到只要有export data
就能够完成后续的编译工作;同时通过实现不同的 pkgReader,便可以从export data
中提取出不同的信息。例如编译器需要从中读取完整的 IR;x/tools
下的工具需要对代码进行静态分析,那么就可以实现一个 pkgReader 来提取自己需要的信息,而不必再自己实现一遍词法语法分析以及类型检查。
在 pkgReader
构建 IR 的过程中,遇到函数类型的 Object 时,做了如下处理:
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/noder/reader.go#L750
2case pkgbits.ObjFunc:
3 if sym.Name == "init" {
4 sym = Renameinit()
5 }
6...
可见 init 函数是多么特殊,它会被重命名,这样就不会与其他 init 函数冲突了。Renameinit() 的实现如下:
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/noder/noder.go#L419
2var renameinitgen int
3
4func Renameinit() *types.Sym {
5 s := typecheck.LookupNum("init.", renameinitgen)
6 renameinitgen++
7 return s
8}
可见只是给了个编号,重命名成了一系列 init.0 init.1 init.2 等等的函数。
至此 LoadPackage()
的工作就完成了。
pkginit.MakeInit()
接下来终于来到了 pkginit 包的内容。
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/gc/main.go#L59
2// Main parses flags and Go source files specified in the command-line
3// arguments, type-checks the parsed Go package, compiles functions to machine
4// code, and finally writes the compiled package definition to disk.
5func Main(archInit func(*ssagen.ArchInfo)) {
6 ...
7 // Parse and typecheck input.
8 noder.LoadPackage(flag.Args())
9 ...
10 // Create "init" function for package-scope variable initialization
11 // statements, if any.
12 //
13 // Note: This needs to happen early, before any optimizations. The
14 // Go spec defines a precise order than initialization should be
15 // carried out in, and even mundane optimizations like dead code
16 // removal can skew the results (e.g., #43444).
17 pkginit.MakeInit()
18 ...
19 // Build init task, if needed.
20 if initTask := pkginit.Task(); initTask != nil {
21 typecheck.Export(initTask)
22 }
23 ...
从注释也可以知道,在词法分析、语法分析以及类型检查和构造 IR 树的过程中,均未涉及代码优化。以下是 MakeInit() 的内容,关键部分使用中文进行了更详细的注释,可以对照相关方法的源码进行阅读。
1// TODO(mdempsky): Move into noder, so that the types2-based frontends
2// can use Info.InitOrder instead.
3func MakeInit() {
4 // Init 相关的处理只涉及全局声明(Package Level),依赖关系作为有向边来构建有向图,然后进行拓扑排序。
5 nf := initOrder(typecheck.Target.Decls)
6 if len(nf) == 0 {
7 return
8 }
9
10 // Make a function that contains all the initialization statements.
11 base.Pos = nf[0].Pos() // prolog/epilog gets line number of first init stmt
12 // 查找 init 符号,如果 Package 的全局符号中没有则创建;不用担心 init 符号已经被用户的 init 函数使用,因为 IR 树在生成过程中遇到 init 会重命名为 init.0 init.1 这样的格式,前面提到 g.generate() 的时候也有说明。
13 initializers := typecheck.Lookup("init")
14 /* 用 init 符号声明一个新的函数,用于存放所有的初始化工作。具体实现是:此处在 IR 树中对应位置建立了新的 ONAME Node(ONAME 表示 var/func name),类型指定为 PFUNC,同时也将符号表中的 init 更新为 symFunc,表明这个符号是函数名;然后新建一个函数节点,将 ONAME Node 指向函数节点,最后将函数节点返回。
15 */
16 fn := typecheck.DeclFunc(initializers, nil, nil, nil)
17 // 类型检查过程中生成了一个 InitTodoFunc,其作为全局初始化语句的临时上下文环境。现在将临时环境 InitTodoFunc 的内容转移到 fn。
18 for _, dcl := range typecheck.InitTodoFunc.Dcl {
19 dcl.Curfn = fn
20 }
21 fn.Dcl = append(fn.Dcl, typecheck.InitTodoFunc.Dcl...)
22 typecheck.InitTodoFunc.Dcl = nil
23
24 // Suppress useless "can inline" diagnostics.
25 // Init functions are only called dynamically.
26 fn.SetInlinabilityChecked(true)
27
28 // 配置函数体。
29 fn.Body = nf
30 typecheck.FinishFuncBody()
31
32 // 确定 fn 为函数节点
33 typecheck.Func(fn)
34 // 在 fn 的内部上下文环境下检查函数体
35 ir.WithFunc(fn, func() {
36 typecheck.Stmts(nf)
37 })
38 // 把函数加入到 Package 的全局声明列表。
39 typecheck.Target.Decls = append(typecheck.Target.Decls, fn)
40
41 // Prepend to Inits, so it runs first, before any user-declared init
42 // functions.
43 typecheck.Target.Inits = append([]*ir.Func{fn}, typecheck.Target.Inits...)
44
45 if typecheck.InitTodoFunc.Dcl != nil {
46 // We only generate temps using InitTodoFunc if there
47 // are package-scope initialization statements, so
48 // something's weird if we get here.
49 base.Fatalf("InitTodoFunc still has declarations")
50 }
51 typecheck.InitTodoFunc = nil
52}
旧版本中 MakeInit() 的工作是在 pkginit.Task() 中实现的,现在被抽取了出来,原因有以下几点。
首先,MakeInit() 负责初始化函数的创建并插入 typecheck.Target.Inits,pkginit.Task() 得到了简化,毕竟这个初始化函数和其他用户定义的 init 实际上没有本质区别。
其次,敏锐的同学可能发现了,类型检查的过程中,已经进行了一次 initOrder(),但只检查了循环依赖的问题;这次又 initOrder() 显得有些冗余。因此将这一部分从 Task() 中拆分出来,希望以后能够放到类型检查的过程中,避免重复的排序操作。当前抽离成了单独的函数但是还未并入类型检查,处于中间状态,可见不久后将会并入类型检查,注释中的 TODO 就是在说这个问题。
最后,初始化函数如果在 Task() 中创建,则无法参与到类型检查结束到 Task() 开始这之间的优化过程,主要包括无效代码清理和内联优化。因此将其提前到类型检查结束后创建,这样就可以参与到优化过程中了。
pkginit.Task()
最后,终于来到了 init 处理的终点, pkginit.Task()。
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/cmd/compile/internal/pkginit/init.go#L93
2// Task makes and returns an initialization record for the package.
3// See runtime/proc.go:initTask for its layout.
4// The 3 tasks for initialization are:
5// 1. Initialize all of the packages the current package depends on.
6// 2. Initialize all the variables that have initializers.
7// 3. Run any init functions.
8func Task() *ir.Name {
9 ...
10 // Find imported packages with init tasks.
11 // 这里可以看出 Package 最终的初始化任务被合并在了 .inittask 这个结构体中,因此对于引用的包才能这样进行查找,此处还检查了 .inittask 结构体是否合法。最终加入到 deps 数组。
12 for _, pkg := range typecheck.Target.Imports {
13 n := typecheck.Resolve(ir.NewIdent(base.Pos, pkg.Lookup(".inittask")))
14 if n.Op() == ir.ONONAME {
15 continue
16 }
17 if n.Op() != ir.ONAME || n.(*ir.Name).Class != ir.PEXTERN {
18 base.Fatalf("bad inittask: %v", n)
19 }
20 deps = append(deps, n.(*ir.Name).Linksym())
21 }
22 ...
23 // 如果开启了 Address Sanitizer,那么需要在创建一个 init 函数加入 typecheck.Target.Inits,用于初始化 ASan 相关的全局变量。
24 if base.Flag.ASan {
25 ...
26 // 可见这个 init 将会在最后执行
27 typecheck.Target.Inits = append(typecheck.Target.Inits, fnInit)
28 }
29 ...
30 // Record user init functions.
31 for _, fn := range typecheck.Target.Inits {
32 // 只有处理 Package 全局变量的才叫 init,其它的都被重命名为了 init.0、init.1 等。
33 if fn.Sym().Name == "init" {
34 // Synthetic init function for initialization of package-scope
35 // variables. We can use staticinit to optimize away static
36 // assignments.
37 s := staticinit.Schedule{
38 Plans: make(map[ir.Node]*staticinit.Plan),
39 Temps: make(map[ir.Node]*ir.Name),
40 }
41 for _, n := range fn.Body {
42 s.StaticInit(n)
43 }
44 fn.Body = s.Out
45 ir.WithFunc(fn, func() {
46 typecheck.Stmts(fn.Body)
47 })
48
49 if len(fn.Body) == 0 {
50 fn.Body = []ir.Node{ir.NewBlockStmt(src.NoXPos, nil)}
51 }
52 }
53
54 // Skip init functions with empty bodies.
55 if len(fn.Body) == 1 {
56 if stmt := fn.Body[0]; stmt.Op() == ir.OBLOCK && len(stmt.(*ir.BlockStmt).List) == 0 {
57 continue
58 }
59 }
60 fns = append(fns, fn.Nname.Linksym())
61 }
最终 fns 数组保存了所有 init 函数;deps 数组保存了所有依赖的包的 .inittask 结构体。接下来合并构建自己 Package 的 .inittask。
1// Make an .inittask structure.
2sym := typecheck.Lookup(".inittask")
3task := typecheck.NewName(sym)
4// 显然这个 .inittask 不是 uint8 类型的,只是为了占位,因此这里设置了一个 fake type。
5task.SetType(types.Types[types.TUINT8]) // fake type
6task.Class = ir.PEXTERN
7sym.Def = task
8lsym := task.Linksym()
9ot := 0
10// lsym.P = [0]
11ot = objw.Uintptr(lsym, ot, 0) // state: not initialized yet
12// lsym.P = [0, len(deps)]
13ot = objw.Uintptr(lsym, ot, uint64(len(deps)))
14// lsym.P = [0, len(deps), len(fns)]
15ot = objw.Uintptr(lsym, ot, uint64(len(fns)))
16// lsym.R = [newR(d)...]
17for _, d := range deps {
18 ot = objw.SymPtr(lsym, ot, d, 0)
19}
20// lsym.R = [newR(d)..., newR(f)...]
21for _, f := range fns {
22 ot = objw.SymPtr(lsym, ot, f, 0)
23}
24// An initTask has pointers, but none into the Go heap.
25// It's not quite read only, the state field must be modifiable.
26// 此处说明这个 .inittask 符号是全局的,决定了最后在 object 文件中的位置区域。
27objw.Global(lsym, int32(ot), obj.NOPTR)
28return task
在最后将其设置为导出的(Export),因为其符号名并非大写字母开头,但是要被其他包使用:
1// Build init task, if needed.
2if initTask := pkginit.Task(); initTask != nil {
3 typecheck.Export(initTask)
4}
至此,Package 单元对于 init 的处理就结束了,最后 Package 被编译为带有 .inittask 表的 object 文件,这个表中包含了所有的 init.x 函数和依赖的包的 .inittask 结构体指针,要注意这里只知道符号之间的关系,其他包里 init 函数的具体实现是不知道的,需要在链接阶段处理。
链接时,在拥有了所有的 .inittask 包含的具体函数相关信息后,链接器会将其按照依赖关系进行排序,生成一个具体的 mainInittasks 列表供 runtime 使用。此处不再展开这一部分,有兴趣的同学可以自行阅读链接器 inittask 部分的源码:src/cmd/link/internal/ld/inittask.go
,最终 SymbolName 为 go:main.inittasks
。
最终链接生成可执行文件时,inittasks 的地址会给到 src/runtime/proc.go
的 runtime_inittasks 数组变量,然后在runtime.main
函数中被使用:
1// https://github.com/golang/go/blob/d8117459c513e048eb72f11988d5416110dff359/src/runtime/proc.go#L144
2func main() {
3 ...
4 doInit(runtime_inittasks) // Must be before defer.
5 ...
6}
最后回看一开始发现的现象:
[现象]:a/b.go
和 a/c.go
的 init() 函数的执行顺序是按照文件名的字母顺序来的,将 a/b.go
改名后,其文件名顺序排在了 a/c.go
之后,最终 init() 执行也排在了之后。
根源在于编译器在读取源文件时是按照文件系统文件名顺序读入,在处理时也是依文件次序处理的,也就是编译器遇到 init 和 import 的顺序都是由文件名顺序决定的。
虽然有
initOrder()
的存在,但是它不会影响用户定义的init()
的顺序initOrder()
会处理 import 的依赖关系,因此最终各个 Package 的 init 顺序时根据依赖关系决定的。
例如:
1// a1.go
2import "b"
1// a2.go
2import "c"
1// b.go
2import "c"
那么不管 a1.go 和 a2.go 的文件名顺序如何,package c 都会先于 package b 初始化,因为 b 依赖 c。