Go `init` 函数:包初始化顺序到底是怎样的

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

相关推荐
何以解忧,唯有..3 小时前
Go语言中的const:常量声明与iota枚举详解
java·开发语言·golang
geovindu6 小时前
go: Reactor Pattern
开发语言·后端·设计模式·golang·反应器模式
記億揺晃着的那天15 小时前
Java 调用外部 Go 程序的实践:ProcessBuilder 在生产环境中的应用
java·golang·processbuilder
jingling55520 小时前
go | 环境安装和快速入门
开发语言·后端·golang
java_cj1 天前
从kubectl学Visitor模式:如何优雅处理多态数据结构的遍历
云原生·golang·k8s·访问者模式
何以解忧,唯有..1 天前
Go语言类型转换详解:从基础到进阶实践
开发语言·后端·golang
何以解忧,唯有..1 天前
Go 语言指针类型详解:从基础到实战
开发语言·后端·golang
ScilogyHunter1 天前
west init 命令详解
init·zephyr·west
迷茫运维路1 天前
Casbin学习教程
golang·casbin