[备忘] Go init 行为
内容提要
Go 的 init 函数在单 Goroutine 中执行,依赖包的 init 先于引用者,main 最后运行。同包多文件 init 顺序未定义,同文件内自上而下。Go 1.21 起,无引用关系的包按包名字母序初始化。若 init 阻塞,可能并发执行,但依赖关系仍保证顺序。
延伸解读
Go 1.21 对初始化顺序的改进
在 Go 1.20 及之前,如果包 a 同时引用了无直接依赖关系的包 b 和 c,它们的 init 执行顺序是不确定的。从 Go 1.21 开始,这类无引用关系的包会按照包名字母序决定初始化顺序,例如 a 一定在 b 之前,github.com/xxx/xxx 一定在 gitlab.com/xxx/xxx 之前。这一变化让构建和测试结果更可预测,但开发者仍不应依赖未定义的顺序,尤其是同包多文件间的 init 顺序依然未定义。
init 阻塞可能引发并发执行
通常所有 init 函数都在单个 Goroutine 中顺序执行,但如果某个 init 发生阻塞,运行 init 的 Goroutine 可能创建新的 Goroutine,导致部分 init 代码并发运行。此时,无引用关系的包之间不再保证 init 完成的先后顺序。不过,依赖关系仍然有效:如果包 a 依赖包 b,a 的 init 一定在 b 的 init 运行完成后才开始。因此,在 init 中执行阻塞操作(如网络请求、等待通道)需格外谨慎。
同包多文件 init 顺序未定义
同一包内多个文件中的 init 函数执行顺序是未定义的,而同一文件内的多个 init 则按自上而下的顺序运行。这意味着如果初始化逻辑分散在多个文件中,且它们之间存在隐式依赖,就可能出现难以排查的问题。建议将强依赖的初始化集中到同一文件,或通过显式调用函数来保证顺序,避免依赖文件级的执行次序。
Q&A
Go 的 init 函数在什么情况下会并发执行?
如果 init 函数中存在阻塞操作,运行 init 的 goroutine 可能会创建新的 goroutine,导致某些 init 代码并发运行。但即使存在阻塞,如果包 a 依赖包 b,a 的 init 仍会在 b 的 init 完成后才开始运行。
Go 1.21 对无引用关系的包初始化顺序做了哪些改变?
在 Go 1.21 及之后,对于无引用关系的包,会按照包名字母序决定初始化顺序。例如包 a 一定在包 b 之前执行,github.com/xxx/xxx 一定在 gitlab.com/xxx/xxx 之前执行。而在 Go 1.20 及之前,这些包的初始化顺序是不确定的。
同一个包内多个文件的 init 函数执行顺序是怎样的?
同一包中多个文件之间的 init 执行顺序是未定义的,但同一文件内的多个 init 函数会按照自上而下的顺序依次执行。
Go 程序启动时,init 函数和 main 函数的执行顺序是怎样的?
执行顺序为:先执行所有依赖包的 init 函数,然后执行 main 包的 init 函数,最后执行 main 包的 main 函数。即 package 的 init -> main 的 init -> main 的 main。
如果包 a 引用了包 b 和包 c,且 b 和 c 之间没有引用关系,它们的 init 执行顺序如何确定?
在 Go 1.20 及之前,b 和 c 的 init 执行顺序是不确定的;从 Go 1.21 开始,会按照包名字母序决定顺序,例如包 b 在包 c 之前执行。
init 函数是否都在同一个 Goroutine 中执行?
通常情况下,所有的 init 函数都在一个 Goroutine 中执行。但如果 init 函数中存在阻塞,运行 init 的 goroutine 可能会创建新的 goroutine,导致某些 init 代码并发运行。