go中init()执行顺序严格按包依赖图拓扑序,变量初始化按依赖就绪性,init()函数则按文件名字典序;跨包需显式import,跨文件init依赖不可靠,应避免i/o等副作用操作。

包导入链决定跨包初始化先后
Go 启动时,init() 执行顺序不是按 import 语句书写顺序,而是严格按依赖图的拓扑序:被依赖的包一定先完成全部初始化(变量 + 所有 init()),再轮到当前包。
常见错误现象:panic: runtime error: invalid memory address,比如 dao 包里某个 init() 函数用了 config 包导出的全局变量,但 config 没被显式 import —— 它只是被 dao 间接依赖,而 Go 不会自动初始化未出现在 import 列表里的包。
- 必须显式
import "xxx"或import _ "xxx",才能触发其初始化 - 循环 import(A import B 且 B import A)直接编译失败,不会进入运行时
-
_ "github.com/lib/pq"这类空导入,目的就是触发其init()注册驱动,不是为了用它的符号
同一包内变量和 init 的执行节奏不一致
变量初始化和 init() 函数不是“一起跑完再跑下一个”,而是分阶段:所有包级变量先按依赖就绪性逐个初始化(非简单声明顺序),之后才统一执行所有 init() 函数。
关键区别在于:变量初始化支持依赖分析(A 用 B,B 就必须先于 A 初始化),而 init() 只认文件名排序(a.go 里的 init() 总在 b.go 之前),不识别函数体内的引用关系。
- 别在
a.go的init()里直接用b.go声明的变量,除非你用命名前缀(如01_config.go)强制字典序 - 更稳妥的做法:把变量和初始化逻辑封装成函数,比如
InitDB(),在main()开头显式调用 - 类型定义(
type X struct{})在变量初始化前就已就绪,所以var x X可以写在类型定义之前
多个文件时 init 执行只看文件名,不看 import 或内容
一个包含 db.go、cache.go、router.go 三个文件,哪怕 router.go import 了 db.go 里的符号,Go 依然按 cache.go → db.go → router.go(字母序)执行它们各自的 init()。
这就导致一种典型坑:你在 db.go 里写了 var DB *sql.DB 并在 init() 里赋值;又在 cache.go 的 init() 里尝试用 DB.Ping() —— 如果 cache.go 字母序排在 db.go 前面,DB 还是 nil,直接 panic。
- 文件名排序不可靠,重构时改名就可能打破初始化链
- 避免跨文件强依赖
init(),尤其不要让一个init()依赖另一个文件里未导出的变量 - 测试文件(
*_test.go)的init()不参与主程序初始化,go test会单独走一遍自己的初始化流程
init 里不能做的事比能做的更重要
init() 是黑盒执行时机:没参数、没返回值、不能 defer、不能 recover、不能重试。一旦出错,进程立即终止,连完整栈都未必打全。
它适合做无副作用、无外部依赖、纯内存操作的事,比如注册驱动、设置常量映射、预计算只读数据。但凡涉及 I/O、网络、锁、goroutine、或调用其他包未初始化的变量,风险陡增。
- 别在
init()里读配置文件——文件可能不存在,路径可能不对,错误无法友好提示 - 别在
init()里连数据库——连接失败无法重试,也难以注入 mock - 别在
init()里启动 goroutine——它可能在main()开始前就往未初始化的 channel 发送数据 - 真正需要顺序控制的初始化逻辑,应该收束到
main()开头,用明确的函数调用链表达依赖
初始化顺序本身不难记,难的是它把隐式依赖藏得太深。越想靠 init() “自动搞定”,越容易掉进文件名排序或依赖分析的缝隙里。显式优于隐式,这句话在 Go 初始化这件事上,特别实在。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











