同一包内多个init函数按文件名字典序执行,同一文件内按代码从上到下顺序执行;跨包按依赖图拓扑序执行,被依赖包init先于依赖包,main包init最后运行。

init 函数的执行顺序不是靠 import 语句顺序或人眼代码顺序决定的,而是由编译器静态分析包依赖图 + 文件名排序共同确定的;跨包访问未初始化变量、循环 import、多文件间隐式依赖,是 panic 最常见源头。
同一包内多个 init 函数怎么执行
同一文件里的多个 init 按代码从上到下顺序执行;同一包不同文件的 init,按文件名**字典序**执行(a.go 在 b.go 前,01_db.go 在 02_router.go 前)。这不是约定俗成,而是 Go 编译器当前稳定行为(Go 1.21+ 默认启用 GOEXPERIMENT=fileorder 后更明确)。
常见错误现象:a.go 的 init 引用了 b.go 声明的全局变量 dbConn,但 b.go 字典序靠后,导致 dbConn 还是零值,后续调用 dbConn.Ping() 直接 panic。
- 别把强依赖逻辑拆到不同文件,尤其当一个
init明确需要另一个文件的变量时 - 如果必须分文件,用数字前缀强制排序(如
01_config.go、02_db.go),但这是权宜之计 - 更推荐:合并为单个
init,或改用普通函数(如Setup())显式调用
跨包 init 执行顺序怎么判断
编译器按**导入依赖图的拓扑序**执行:被依赖包的 init 一定先于依赖它的包执行。比如 pkg/httpserver 导入了 pkg/config,那么 pkg/config.init 必然在 pkg/httpserver.init 之前完成。
立即学习“go语言免费学习笔记(深入)”;
常见错误现象:pkg/httpserver.init 里直接用了 config.Port,但 pkg/config 的 init 因某种原因没跑(比如该包只被 import _ 触发,而符号没被实际引用),结果 config.Port 是 0,http.ListenAndServe(":0") 绑定失败或监听了错误端口。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
import _ "net/http/pprof"会触发net/http/pprof的init,且它发生在当前包所有变量声明和init之前 - main 包的
init总是最后执行(因为不被其他包 import) - 间接依赖(A→B→C)不会自动触发 C 的
init,除非 C 被 A 或 B 显式 import
为什么 init 里访问其他包变量容易 panic
因为跨包变量初始化没有语言级保障。你写的 var Port = os.Getenv("PORT") 看似简单,但它依赖 os 包已就绪;而 os 自身的 init 是否已完成,取决于它是否被当前初始化链“需要”——这很容易漏掉。
常见错误现象:panic: runtime error: invalid memory address or nil pointer dereference,堆栈指向某行日志打印或数据库连接语句,根源却是上游配置包的 init 没执行,log.Logger 或 sql.DB 仍是 nil。
- 别在
init中调用其他包的导出函数(如log.SetOutput),除非确认它不依赖未初始化的内部状态 - 同一包内跨文件访问也危险:a.go 的
init引用 b.go 的变量,而 b.go 文件名字典序靠后 → 变量未初始化 - 变量初始化表达式(如
var port = os.Getenv("PORT"))在同文件init前执行,但如果它调了别的包函数,照样可能 panic
哪些操作绝对不该放在 init 里
init 是包加载阶段的临界区,任何阻塞、IO、网络、锁、goroutine 启动后不等待,都会破坏整个程序启动流程,且无法 recover。
常见错误现象:本地运行正常,容器部署后卡住无日志输出;或测试时偶尔 panic,CI 频繁失败;根本原因是 init 里做了不可控耗时操作。
- 启动 HTTP server:
http.ListenAndServe会永久阻塞,导致后续init永远等不到返回 - 连接数据库/Redis:
sql.Open不连接,但db.Ping()会真实发起网络请求,超时或失败即 panic - 读取大文件或解析复杂 YAML:
ioutil.ReadFile+yaml.Unmarshal可能慢且易错,错误难以定位 - 调用
time.Sleep或任何同步等待逻辑:违背init的轻量本意
真正安全的做法是把初始化逻辑拆成显式函数(如 config.Load()、db.Connect()),在 main() 中按需、可控、可重试地调用——顺序、错误处理、日志、超时,全由你掌握。

















