init函数在包被导入时自动且仅执行一次,按依赖拓扑排序(被依赖包优先)、同文件内按声明顺序、不同文件间按文件名字典序执行;不可带参、无返回、不可显式调用;禁止阻塞、跨包强依赖或易错外部操作。

init 函数什么时候被调用?
init 函数在包被导入时自动执行,且仅执行一次。它不是按文件顺序、而是按依赖拓扑排序后执行:被依赖的包先执行 init,主包(main)的 init 最晚运行。这意味着如果你在 utils 包里初始化了一个全局 sync.Once,又在 httpserver 包里依赖它,那 utils.init 一定早于 httpserver.init。
常见错误现象:panic: runtime error: invalid memory address —— 某个包的 init 试图访问另一个尚未初始化的包级变量。
- 不要在
init中调用其他包的导出函数,除非你确认其包已声明依赖(即已 import)且无循环依赖 - 如果必须跨包初始化,优先用懒加载(如
sync.Once+ 函数封装),而非强依赖init顺序 -
init不能带参数、不能有返回值、不能被显式调用 —— 它只属于 Go 运行时调度
多个 init 函数的执行顺序怎么确定?
同一个文件里可以有多个 init 函数,它们按出现顺序执行;不同文件之间,则按源文件名的字典序执行(Go 1.19+ 默认启用 go mod tidy 后的排序逻辑,但文件名仍是关键)。这不是语言规范保证的行为,而是当前编译器实现的事实。
使用场景:比如你想让配置加载早于日志初始化,又希望日志初始化早于数据库连接,就得靠文件命名控制,例如:01_config_init.go、02_log_init.go、03_db_init.go。
立即学习“go语言免费学习笔记(深入)”;
- 别依赖文件名排序做核心逻辑 —— 它脆弱,CI 环境或不同 go 版本可能表现不一致
- 真正需要顺序的初始化动作,应合并进单个
init或拆成显式初始化函数(如SetupConfig()),由main显式调用 - 测试时容易踩坑:
go test会单独编译测试包,若测试文件也含init,它和被测包的init执行时机可能错乱
init 里做 HTTP server 启动?危险操作
在 init 中启动 http.ListenAndServe 是典型反模式。它会阻塞包初始化流程,导致依赖该包的其他代码永远等不到 init 返回,进而卡死整个程序启动。
错误示例:
func init() {
http.ListenAndServe(":8080", nil) // 这里就卡死了
}
-
init必须快速返回,不能含任何阻塞 I/O、无限循环、goroutine 启动后未同步等待 - 想“自动”启动服务?改用
func StartServer(),由main显式调用,并配好 shutdown 逻辑 - 如果真要用 goroutine 启动服务,至少加
go func() { ... }()并确保不会 panic 泄露,但依然不推荐 —— 初始化阶段不该承担运行时职责
为什么 init 里的 panic 会导致程序直接退出?
init 函数中发生的 panic 不会被 recover,Go 运行时会终止整个程序,且不执行任何 defer(包括 main 函数里的)。这是设计使然:包初始化失败意味着程序处于不可用状态,无法安全继续。
性能影响很小,但副作用极重 —— 它让错误提前暴露,但也剥夺了你兜底的机会。
- 避免在
init中做可能失败的外部依赖操作(如读配置文件、连 Redis、解析环境变量) - 把易错逻辑移到
main或初始化函数中,用 error 返回和明确处理路径 - 调试时注意:
go run报错位置常指向init所在行,但实际 panic 可能来自它调用的深层函数,需结合 stack trace 看最内层
init 的本质是「静态契约」:它假设一切就绪,不接受妥协。一旦你开始在里面写 if err != nil { log.Fatal(err) },就说明它已经越界了。










