init函数在包被导入时自动执行且仅一次,总在main前运行;执行顺序按文件名unicode字典序,同文件内按代码自上而下顺序。

init 函数什么时候执行?顺序怎么保证?
init 函数在包被导入时自动执行,且只执行一次;它总是在 main 函数之前运行,但多个 init 的执行顺序有明确规则:先按源文件字典序,再按文件内定义顺序。这意味着如果你有两个文件 config.go 和 db.go,即使 db.go 依赖 config.go 中的变量,也不能假设 config.go 的 init 一定先跑——除非它们在同一个包里且文件名排在前面。
常见错误现象:panic: runtime error: invalid memory address or nil pointer dereference,往往是因为某个 init 里用了另一个包里尚未初始化的全局变量(比如未初始化的 *sql.DB)。
- 同一包中多个
init函数按文件名升序执行,不是按 import 顺序 - 跨包依赖时,Go 只保证被依赖包的
init在依赖包的init之前完成(即 import 链上的拓扑序),但不保证具体时间点 - 避免在
init中调用本包其他未声明的变量(Go 编译器会报错),但可以安全引用已声明、且同文件或更早文件中初始化的变量
init 适合做哪些初始化?哪些绝对不能做?
init 适合做无副作用、幂等、不依赖外部状态的包级准备动作,比如注册驱动、预设常量映射、初始化只读配置结构体。它不适合做需要返回错误、重试、或依赖网络/磁盘 I/O 的操作——因为一旦失败,程序直接崩溃,无法捕获或恢复。
典型使用场景:database/sql 包里各数据库驱动通过 init 调用 sql.Register;net/http 中某些中间件包用 init 向默认 mux 注册 handler。
- ✅ 可以:设置全局
sync.Once、初始化var config = struct{...}{}、调用flag.StringVar - ❌ 禁止:打开文件、连接数据库、发起 HTTP 请求、启动 goroutine(除非你明确控制其生命周期)
- ⚠️ 高风险:读取环境变量并赋值给全局变量——如果环境未就绪,会导致静默错误或不可预测行为
如何安全地在 init 中处理可能失败的初始化?
Go 的 init 函数没有返回值,也不能 panic 后恢复。所以任何可能失败的操作,都必须转为“延迟失败”策略:把初始化逻辑移到一个导出函数中,由调用方显式触发,并返回 error;init 只负责注册或设置钩子。
例如,不要在 init 里调用 os.Open,而是定义 func LoadConfig() error,并在 main 开头调用它。若真想让包“自带初始化能力”,可用 sync.Once + 懒加载:
var (
cfg Config
once sync.Once
err error
)
func init() {
// 只注册,不执行
}
func GetConfig() (Config, error) {
once.Do(func() {
cfg, err = loadConfigFromEnv()
})
return cfg, err
}
- 所有带 I/O 或外部依赖的初始化,必须移出
init,改用显式函数调用 - 若坚持用
init做简单校验(如检查必要环境变量是否存在),建议用log.Fatal明确退出,比静默 panic 更易排查 - 测试时,
init会在测试开始前执行,因此无法 mock 或跳过——这也是反对在init中做实际工作的另一原因
多个 init 函数之间共享状态要注意什么?
同一包中多个 init 函数共享包级变量作用域,但它们之间没有执行时序保障(仅靠文件名排序)。如果两个 init 都写同一个变量,结果取决于文件名;如果一个读、一个写,就容易读到零值。
例如:文件 a.go 定义 var port int 并在 init 中设为 8080;文件 b.go 的 init 读取 port 并启动 server。但如果 b.go 字典序在 a.go 之前,port 就还是 0,server 监听失败。
- 最稳妥做法:所有初始化相关变量,在声明时直接赋值(如
var port = 8080),而非在init中赋值 - 若必须动态初始化,把所有逻辑集中在一个
init函数里,放在单独的init.go文件中(文件名以_或a_开头确保最先执行) - 避免跨文件读写同一变量——这本质上是隐式依赖,难以维护和测试
main 或显式初始化函数来掌控。











