go的init函数执行顺序由包依赖图拓扑序决定,被依赖包init必先于依赖包;同包内按文件名字典序、同一文件内按源码声明顺序执行。

Go 的 init 函数执行顺序是确定的,但依赖图比 import 语句更底层——别靠 import 顺序猜 init 时机,否则 nil panic 很快找上门。
包级 init 执行顺序由依赖图决定,不是 import 顺序
你写 import "a"; import "b",不代表 a.init() 一定先于 b.init()。真正起作用的是包之间的符号引用关系:如果 b 包里用了 a.SomeVar,那么 a 就是 b 的依赖,a.init() 必然在 b.init() 前执行,哪怕 b 在 import 列表里排第一。
常见错误现象:panic: runtime error: invalid memory address or nil pointer dereference 出现在 handler.init() 里,但真正原因是它依赖的 db 包还没跑完 init,db.DB 还是 nil。
- 用
go tool compile -S main.go | grep INIT可看到编译器生成的实际初始化调用链 - 测试时所有
*_test.go共享一次包初始化,但若测试中启 goroutine 并发改全局状态,可能暴露时序问题 -
import _ "xxx"触发的初始化也遵守依赖顺序,不是“一 import 就立刻 init”
同一包内多个 init 按文件字典序 + 源码顺序执行
同一个包下,a.go 里的 init 一定比 b.go 里的先执行;同一文件里,上面写的 init 一定比下面写的先执行。
但你不该依赖这个顺序做逻辑耦合——比如让 init1 初始化配置,init2 读配置。一旦文件重命名或拆分,行为就变了。
- 包级全局变量初始化(如
var x = f())发生在所有init之前,且按源码书写顺序执行 - 标准库如
log已处理自身init时序,但自定义包若在init中调用其他包未初始化完的函数(比如自己写的 logger),容易 crash - 避免在
init里用reflect.Value.FieldByName("X")这类反射访问跨包变量——它不触发依赖包初始化,依赖链直接断裂
main 函数前 init 执行完毕,但不能假设所有包都已初始化
main 函数开始执行时,所有被显式或隐式引用的包的 init 确实已完成。但如果你通过字符串拼接、环境变量加载路径、插件式注册等绕过编译期符号引用,那些包就不会进依赖图,init 根本不会触发。
典型场景:你在 plugin.Load("db/mysql") 里用 sql.Open,但没在任何地方直接引用 mysql 包,它的 init 就永远不会跑,驱动也不会注册。
- 检查是否用了
database/sql.Register类型注册,这类调用通常放在init里,必须被依赖图捕获 - 延迟初始化推荐用
sync.Once+ 显式函数,比如func GetDB() *sql.DB,而不是把所有逻辑塞进init - 跨包全局变量(如
var DB *sql.DB)若只在运行时动态加载,务必确保初始化路径上无未触发的init依赖
最易被忽略的点:init 不是“启动时统一执行的一段代码”,而是嵌套在依赖拓扑里的精确节点。你以为安全的顺序,可能只是当前 import 写法碰巧成立;真正可靠的,只有编译器静态分析出的依赖边。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











