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

相关推荐
王的宝库13 小时前
Go 项目结构:从单文件到标准工程布局
开发语言·后端·golang
花酒锄作田14 小时前
Go - Gin中使用sessions
golang
不甘先生19 小时前
Go 中 type、方法与指针接收者:从 str_name.Name() 看懂 Go 的类型系统
开发语言·后端·golang
Casbin开源社区1 天前
一个面板管住 29 个 AI 编程 Agent:Casbin Gateway 的配置统一、协议互转、用量统计与权限管控
人工智能·golang·开源·gateway·casbin
名字还没想好☜1 天前
Go 标准库 flag 包实战:参数解析、子命令、自定义 Value 类型与默认值
开发语言·后端·golang·go
zh73141 天前
Go 1.23 → 1.26 升级检查文档
开发语言·chrome·golang
平头哥AI2 天前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
2601_962296512 天前
如何使用Golang包路径_管理本地和远程模块引用
golang·数据转换·变量定义·常用库·编程语言区别
FfHUCisI2 天前
GMP 调度器:Go 并发的心脏是如何跳动的
开发语言·golang·php
我不会起名字3222 天前
一天一道算法题(26):栈的简单应用
java·数据结构·python·算法·leetcode·golang·