包初始化的顺序有明确的规则,但容易记错:先依赖、后自己;先变量、后 init;变量按依赖而不是书写顺序。这篇用一个真实的小模块把每条规则跑出来,再用 inittrace 看每个包的 init 花了多少,最后列出编译器对初始化的限制。
b: 变量 B 初始化
b: init()
a0.go: 变量 A0 初始化
a1.go: 变量 A 初始化,用到 b.B = b: 变量 B 初始化
a0.go: init()
a1.go: init() #1
a1.go: init() #2(同一个文件里的第二个 init)
c: 变量 C 初始化(c 不依赖 a、b)
c: init()
main.go: 变量 M1 初始化
zz.go: 变量 Z 初始化(文件名排在 main.go 后面)
main.go: init()
zz.go: init()
main(): 开始,a.A = a1.go: 变量 A 初始化,用到 b.B = b: 变量 B 初始化,c.C = c: 变量 C 初始化(c 不依赖 a、b)
go list -deps 给出的依赖顺序(叶子在前):fmt lab/tr lab/b lab/a lab/c lab
go list 报告 a 包的文件顺序:[a0.go a1.go]规范(doc/go_spec.html 的 Package initialization 一节)写的是:把所有包按导入路径排好序,每一步初始化列表里第一个“它导入的包都已初始化”的包,重复到全部完成——所以上面是 b、a、c、main。一个包要等它导入的所有包初始化完才开始;在包内,先初始化所有包级变量,再依次运行所有 init 函数,文件按文件名排序(go list 里 a 包的文件顺序是 [a0.go a1.go]),同一个文件里的多个 init 按书写顺序。上面的 main 包里 zz.go 的变量虽然排在 main.go 之后,仍然在 main.go 的 init 之前初始化。
变量声明顺序是 a、b、c、d;a 依赖 c;b 依赖 f(),f 读取 d:
初始化 c = 10
初始化 a = 11
初始化 d = 100
f() 被调用,读取 d = 100
初始化 b = 101
init():a, b, c, d = 11 101 10 100规范的规则是:反复选择声明顺序里最靠前的、不依赖任何未初始化变量的那个。依赖包括通过函数体间接引用的变量(b 调用 f()、f 读 d,所以 d 先于 b)。实验里的顺序 c、a、d、b 正是这条规则的结果。
go run a.go b.go m.go:
a.go 的 init
b.go 的 init
go run b.go a.go m.go:
b.go 的 init
a.go 的 init
go run .(按文件名排序):
a.go 的 init
b.go 的 init初始化顺序里“按文件名排序”是 go build 这类按包构建时的行为;go run a.go b.go 直接传文件时,编译器收到的就是命令行的顺序,所以交换两个文件名就会交换它们的 init 顺序。不要依赖不同文件里 init 的先后。
标准库与运行时一共有 16 条 init 记录;其中 lab 模块的包:
init lab/b @… ms, 0.037 ms clock, 1672 bytes, 7 allocs
init lab/a @… ms, 0.025 ms clock, 336 bytes, 8 allocs
init lab/c @… ms, 0.001 ms clock, 32 bytes, 2 allocs
init main @… ms, 0.004 ms clock, 64 bytes, 4 allocsGODEBUG=inittrace=1 对排查启动慢很有用:每个包一行,写明起始时间、耗时、分配的字节与次数。本例里每个包都很小;真正的程序里常常能找到一两个在 init 里做了大量工作的包。
初始化循环:
main.go:3:5: initialization cycle for a
通过函数形成的初始化循环:
main.go:3:5: initialization cycle for x
手动调用 init():
main.go:5:15: undefined: init
init 带参数或返回值:
main.go:3:6: func init must have no arguments and no return values
init 作为变量使用:
main.go:3:9: undefined: init初始化循环(包括通过函数形成的)是编译错误;init 不是普通标识符,不能被调用、不能当作值引用,也不能有参数与返回值。