Go 中 init() 何时执行?通过实际示例深入解析
想象一下:你正在用 Go 编写一个微服务。在其中一个包中,你声明了 init() 来加载配置。启动应用程序——它却突然因 nil pointer dereference 而崩溃。你惊慌失措:“为什么?配置明明应该先加载的!”
熟悉吗?如果是,那你并不孤单。“init() 函数何时运行?” 这个问题是 Stack Overflow 上最热门的问题之一(有数千次浏览)。让我们一劳永逸地弄清楚 Go 中 init() 何时以及按何种顺序触发,这样你就不会再掉进这个陷阱了。
什么是 init(),它有什么用?
在 Go 中,init() 是一个没有参数和返回值的特殊函数。它不会被显式调用。Go 运行时会自动在 main() 函数执行之前调用它。
这有什么用?
- 初始化需要复杂逻辑的全局变量(例如,从文件读取)。
- 注册数据库驱动或 HTTP 处理器。
- 检查环境(环境变量、资源可用性)。
但最重要的规则是:你永远不应该依赖同一包中不同文件之间 init() 的执行顺序。原因如下。
init() 究竟何时触发?精确顺序
Go 按照严格规则执行 init()。让我们逐一分解。
规则 #1:同一包内,顺序固定
在同一个包内,init() 按照文件声明的顺序执行(按文件名的字母顺序)。但这是实现细节——最好不要依赖它。
规则 #2:先初始化变量,再执行 init()
在调用 init() 之前,Go 会初始化包中的所有全局变量。如果某个变量依赖于同一包中的另一个变量,那么它们的声明顺序就很重要。
package config
var AppName string = "MyApp"var Version string = getVersion() // 在 init() 之前调用
func init() { // 这里 AppName 和 Version 已经可用 println("Init started for", AppName)}
func getVersion() string { return "1.0.0"}规则 #3:导入的包先初始化
这是最重要的规则。如果包 A 导入了包 B,那么包 B 中的所有 init() 会先执行,然后才是包 A 中的。Go 会构建依赖图并“自底向上”执行初始化。
// 包 databasepackage database
func init() { println("1. database init")}
// 包 main(导入 database)package main
import "myapp/database"
func init() { println("2. main init")}
func main() { println("3. main function")}
// 输出:// 1. database init// 2. main init// 3. main function规则 #4:一个文件中可以有多个 init()——但没必要
一个文件中可以有多个 init()。它们会按声明顺序执行。但这会使代码难以阅读。最好将逻辑合并到一个函数中。
实际示例:如果顺序搞错了会怎样?
让我们模拟一个典型的新手错误。假设我们有一个 config 包,它从文件读取设置,以及一个 app 包,它使用这些设置。
// config.gopackage config
var Settings map[string]string
func init() { // 模拟读取文件 Settings = map[string]string{ "port": "8080", "host": "localhost", } println("Config loaded")}
// app.gopackage app
import "myapp/config"
var ServerPort string
func init() { // 错误:如果 config.Settings 尚未初始化