本文中引用的源码均标注了 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.goa/c.goinit() 函数的执行顺序是按照文件名的字母顺序来的,将 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/types2go/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 时使用 types2go/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 进行类型检查,因此 nodernoder2 仍然是并存的。

  • 在 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

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 的导出;接下来使用 pkgReaderdata 重新构建为 IR,存储在 typecheck.Target

明明步骤紧接在一起,为什么要把 Unified IR 先 exportimport 呢? 这样做主要是为了将 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() 中实现的,现在被抽取了出来,原因有以下几点。

  1. 首先,MakeInit() 负责初始化函数的创建并插入 typecheck.Target.Inits,pkginit.Task() 得到了简化,毕竟这个初始化函数和其他用户定义的 init 实际上没有本质区别。

  2. 其次,敏锐的同学可能发现了,类型检查的过程中,已经进行了一次 initOrder(),但只检查了循环依赖的问题;这次又 initOrder() 显得有些冗余。因此将这一部分从 Task() 中拆分出来,希望以后能够放到类型检查的过程中,避免重复的排序操作。当前抽离成了单独的函数但是还未并入类型检查,处于中间状态,可见不久后将会并入类型检查,注释中的 TODO 就是在说这个问题。

  3. 最后,初始化函数如果在 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.goa/c.go 的 init() 函数的执行顺序是按照文件名的字母顺序来的,将 a/b.go 改名后,其文件名顺序排在了 a/c.go 之后,最终 init() 执行也排在了之后。

根源在于编译器在读取源文件时是按照文件系统文件名顺序读入,在处理时也是依文件次序处理的,也就是编译器遇到 init 和 import 的顺序都是由文件名顺序决定的。

  1. 虽然有 initOrder() 的存在,但是它不会影响用户定义的 init() 的顺序

  2. 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。