init函数在包首次被导入时自动执行且仅一次,不接受参数、无返回值、不可显式调用;触发条件为import、空白导入或引用导出符号,执行时机在包级变量初始化后、main函数前,按依赖拓扑序和文件名字典序确定顺序。

init函数执行时机和触发条件
init函数不是在main之前统一跑一遍,而是包被首次导入时才触发,且只执行一次。它不接受参数、无返回值、不能显式调用——哪怕你写init()也会编译报错。
关键触发点有三个:import "pkg"、import _ "pkg"(空白导入)、或当前包代码中直接引用了pkg的导出符号(比如调用pkg.Func())。只要没被真正“需要”,那个包的init永远不会运行。
- 如果只是
import _ "net/http/pprof"但没起HTTP server,pprof的init仍会执行(它往http.DefaultServeMux注册路由) -
go test可能多次初始化同一包:每个测试文件单独编译加载,init会重复跑,容易导致端口冲突或数据库连接复用失败 - Go 1.20+ 对未使用包做裁剪优化:若
import _ "xxx"后完全没引用其任何符号,该包可能被整个丢弃,init也不会执行
全局变量初始化和init的先后关系
包级变量初始化表达式(如var port = os.Getenv("PORT"))总是在同文件所有init函数之前执行,但仅限本文件内声明的变量;跨文件时,顺序由文件名决定,不是代码书写位置。
常见翻车点:你在a.go里写var db *sql.DB = newDB(),而newDB()依赖b.go里定义的config.URL——如果b.go字典序靠后,config.URL还是零值,newDB()就panic。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 同一文件内,变量初始化严格按声明顺序:先声明的变量,其初始化表达式先求值
- 同一包不同文件间,变量初始化也受文件名字典序约束:
01_config.go里的变量一定比02_db.go里的先初始化 - 不要在变量初始化表达式里调用其他包函数(如
config.Load()),除非你能100%确认那个包已完成初始化 - 循环初始化会直接编译失败:
var a = b + 1和var b = a * 2放一起,报initialization loop
跨包init执行顺序怎么确定
编译器构建包依赖图(DAG),按拓扑序执行:被依赖包的init一定先于依赖它的包。这个顺序只看直接import,不追踪间接依赖。
比如main导入db和cache,而db又导入log,cache也导入log——那么log.init()一定最先执行,db.init()和cache.init()谁先谁后**未定义**,每次build可能不同。
-
import _ "github.com/lib/pq"必须出现在database/sql.Open("postgres", ...)之前,否则驱动未注册,报错sql: unknown driver "postgres" - A → B → C → A 是循环import,编译阶段就失败,根本不会走到
init阶段 - 标准库(如
fmt、os)的init是安全的,但自定义包若在init里调用log.Printf,得确认log包已初始化完毕 - 调试时可在每个
init开头加log.Println("init in xxx"),观察输出顺序验证实际执行流
为什么init里连数据库/读配置常失败
因为init是包加载临界区,不是业务启动脚本。它要求所有操作无副作用、不阻塞、不依赖运行时输入——但连数据库、读环境变量、解析YAML恰恰全踩中这些雷。
典型现象:panic: runtime error: invalid memory address or nil pointer dereference,本质是某个init访问了另一个还没初始化的全局变量(比如config.Port为0或nil),或者http.ListenAndServe永久阻塞,卡死后续所有初始化。
- 别在
init里做IO:打开文件、连DB、请求HTTP API、sleep等待资源就绪——这些都不可控且无法recover - 别起goroutine指望它在
main前完成:没有同步机制,main开始时goroutine可能还在跑 - 真正可靠的方案是把初始化逻辑拆成显式函数,如
func Init() error,用sync.Once保证幂等,在main里按需顺序调用:config.Init()→db.Init()→router.Init() - 如果非要用
init,只做纯内存操作:注册驱动、设置默认值、预计算常量——越轻量越安全
复杂点在于:文件名字典序是当前编译器实现行为,不是语言规范保证;跨包依赖顺序虽稳定,但间接依赖不自动触发;而测试环境下的重复初始化,往往比生产环境更早暴露问题。

















