Go init 函数:包初始化顺序到底是怎样的
在 Go 里,init 函数经常被用来做配置注册、默认值初始化、驱动加载等工作。但很多人第一次接触时都会有一个疑问:
main 包、依赖包、包变量、多个 init 函数,它们到底谁先执行?
结论先说:
- 先初始化包级变量
- 再执行包内的
init - 依赖链越深的包越先初始化
- 同一个包只会初始化一次
- 最后才轮到
main.main
一个简单例子
package2
go
package package2
import "fmt"
var Pkg2Var = initPkg2Var()
func initPkg2Var() string {
fmt.Println("package2: var init")
return "pkg2"
}
func init() {
fmt.Println("package2: init")
}
package1
go
package package1
import (
"fmt"
// 通过下划线的方式导入init函数
_ "package2"
)
var Pkg1Var = initPkg1Var()
func initPkg1Var() string {
fmt.Println("package1: var init")
return "pkg1"
}
func init() {
fmt.Println("package1: init")
}
main
go
package main
import (
"fmt"
_ "package1"
)
var MainVar = initMainVar()
func initMainVar() string {
fmt.Println("main: var init")
return "main"
}
func init() {
fmt.Println("main: init")
}
func main() {
fmt.Println("main: run")
}
输出结果
package2: var init
package2: init
package1: var init
package1: init
main: var init
main: init
main: run
为什么会这样
Go 编译器在构建程序时,会先把所有被导入的包按依赖关系排好顺序,再逐个初始化。对于单个包而言,初始化阶段也有固定规则:
- 包级变量按声明依赖顺序初始化
- 然后执行该包中的
init - 一个包里可以有多个
init - 一个文件里也可以有多个
init
这里有一个很关键的点:init 的执行顺序不是"谁写在前面谁先跑",而是先看包依赖,再看包内初始化规则。
也就是说,哪怕 main 包里的 init 看起来离程序入口最近,它也必须等所有依赖包都初始化完成后才能执行。
多个 init 会怎么执行
很多人会继续追问:如果一个包里不止一个 init,那顺序怎么定?
答案是:
- 同一个包可以有多个
init - 同一个
.go文件里也可以定义多个init - 它们会在包级变量初始化完成后依次执行
可以看一个简化示例:
go
package demo
import "fmt"
var Name = initName()
func initName() string {
fmt.Println("var init")
return "demo"
}
func init() {
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2")
}
输出结果是:
text
var init
init 1
init 2
这至少说明一件事:包变量一定先于 init 执行。
同一个包有多个文件怎么办
如果一个包下有多个文件,例如:
text
demo/
├── a.go
├── b.go
└── c.go
并且每个文件里都写了包变量或者 init,那它们也会参与同一个包的初始化流程。
通常可以这样理解:
- 先完成这个包里所有需要先做的变量初始化
- 再执行这个包里的
init - 然后才轮到导入它的上层包
在日常开发里,不建议依赖"文件名顺序"去设计业务行为。因为一旦代码拆分、重命名或者调整依赖,初始化顺序就会变得不直观,维护成本会明显升高。
更稳妥的做法是:
init只做简单、确定、无副作用的初始化- 真正重要的启动逻辑放到显式函数里
- 需要错误处理的初始化不要放在
init里硬做
包被重复导入,会执行几次
另一个常见误区是:如果多个地方都导入了同一个包,它的 init 会不会执行多次?
答案是不会。
同一个包在一次程序运行过程中只会被初始化一次。即使它被多个包间接依赖,Go 也只会在第一次需要它的时候完成初始化,后续不会重复执行。
这也是为什么很多注册型框架喜欢利用 init:
- 注册数据库驱动
- 注册编码器/解码器
- 注册插件或扩展点
这类场景的共同特点是:初始化一次就够了。
常见误区
误区一:init 适合放启动主流程
不适合。
init 更适合做轻量级准备工作,而不是承载完整启动流程。因为它没有显式参数,也不能优雅地把初始化失败逐层返回给调用方。
误区二:init 越多越方便
短期看方便,长期看通常会让程序启动顺序越来越隐蔽。尤其是项目变大以后,排查"为什么这个配置已经生效""为什么那个对象提前创建了"会变得很麻烦。
误区三:所有初始化都应该自动完成
并不是。
如果初始化动作依赖外部资源,比如数据库、Redis、配置中心、远程服务,那么更推荐显式写成:
go
func NewService() (*Service, error) {
// do init here
return &Service{}, nil
}
这样错误处理、重试、日志和测试都会更清晰。
实战建议
- 不要把复杂业务逻辑塞进
init - 避免在
init里做不可控的外部依赖调用 - 配置注册、轻量级默认值初始化更适合放在这里
- 如果初始化失败需要明确返回错误,优先考虑显式初始化函数
总结
Go 的初始化顺序可以记成一句话:
先依赖包,再当前包;先变量,再 init;最后 main。
详细源码见https://github.com/JCCGGKS/Golang_resource/tree/master/init