[备忘] Go init 行为

💡 原文中文,约1000字,阅读约需3分钟。
📝

内容提要

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 代码并发运行。

🏷️

标签

➡️

继续阅读