Go包初始化顺序由编译器按依赖图拓扑排序决定,被依赖包init必先于依赖包执行;包内多init按文件字典序及源码顺序执行;跨包全局变量未初始化易致nil panic。

Go 包初始化顺序由编译器静态决定,不是按 import 顺序执行
Go 的 init 函数执行顺序是确定的、可预测的,但容易误以为“先 import 就先 init”。实际规则是:编译器根据包依赖图做拓扑排序,**被依赖的包一定在依赖它的包之前完成初始化**。比如 package A 导入 package B,那么 B.init() 必然在 A.init() 之前执行,哪怕 B 是最后一个 import 的。
常见错误现象:panic: runtime error: invalid memory address or nil pointer dereference 出现在某个 init 里,但问题根源其实在它依赖的包里——那个包的 init 没跑完,全局变量还是零值。
- 每个包内多个
init函数按源文件字典序执行(a.go在b.go前) - 同一文件中多个
init按出现顺序执行 -
import _ "xxx"触发的包初始化,也遵守依赖顺序,不是“立即执行”
跨包全局变量初始化失败,往往卡在 init 依赖链断裂
典型场景:你在 db 包里定义了 var DB *sql.DB,并在 init 中调用 sql.Open 初始化;又在 handler 包里直接用了 db.DB.QueryRow。如果 handler 在 db 之前被初始化(比如通过间接 import 或测试文件触发),DB 就是 nil。
这不是 bug,是设计使然:Go 不保证未显式 import 的包被初始化。即使 handler 文件里没写 import "db",只要它引用了 db.DB,编译器就会把 db 加入依赖图——但如果你用反射、字符串拼接或延迟加载绕过编译期引用,依赖就断了。
立即学习“go语言免费学习笔记(深入)”;
- 检查是否用了
reflect.Value.FieldByName("DB")这类方式间接访问,它不会触发包初始化 - 避免在
init中调用其他包尚未初始化的函数(比如log.Printf在log包自己 init 完之前是不安全的,不过标准库已处理,自定义包要小心) - 用
go tool compile -S main.go | grep INIT可看编译器生成的初始化调用序列
测试时 init 执行一次,但并行测试可能暴露竞态
Go 测试默认单进程运行所有 *_test.go,整个过程中每个包的 init 只执行一次。但如果你用 go test -p=4 或在测试中启动 goroutine 并发修改全局状态,就可能撞上未预期的时序问题。
比如一个包的 init 设置了 os.Setenv("MODE", "test"),而另一个包的 init 读取该环境变量来配置 logger——看似没问题,但如果测试本身也改了环境变量且没恢复,后续测试就可能错乱。
- 测试中不要依赖或修改跨包全局变量,尤其那些在
init中初始化的 - 用
testing.T.Cleanup恢复环境变量、关闭 mock server 等副作用 - 对必须共享的状态,考虑用
sync.Once替代init,把初始化时机推迟到首次使用
main 函数开始前,所有 init 已执行完毕
这是最常被忽略的前提:main.main() 是整个程序第一个用户代码入口,此前所有包的 init(包括 main 包自己的)都已完成。所以你永远不该在 main 开头写 “确保初始化” 类逻辑。
但这也意味着:任何在 init 中发生的 panic,都会让程序直接退出,不进 main,也不会触发 defer 或 os.Exit。日志可能来不及刷出,错误信息也较简略。
- 在
init中尽量只做无副作用、无外部依赖的初始化(如预计算 map、注册类型) - 涉及文件、网络、数据库等 I/O 的初始化,移到
main或显式初始化函数中,方便错误处理和重试 - 用
go run -gcflags="-m" main.go可确认哪些变量被分配到堆/栈,辅助判断初始化是否真被编译器优化掉
真正难调试的,往往是 init 之间隐式的依赖关系——比如两个包都 init 时往同一个全局 slice append,谁先谁后取决于它们在依赖图里的位置,而不是 import 顺序。这种耦合一旦形成,改起来比 runtime panic 还麻烦。










