面试官:请问 Golang 中 init 函数是什么时候执行的?它的执行顺序是怎样的?

面试回答

“在 Go 语言中,init 函数是在程序启动时、main 函数执行之前自动执行的,主要用于包级别的初始化工作。

它的执行顺序非常严格,可以从两个维度来看: 首先是包与包之间的顺序。Go 会按照包的依赖关系,采用深度优先的策略进行初始化。比如包 A 依赖包 B,包 B 依赖包 C,那么初始化的顺序就是 C B A。即使一个包被多个包导入,它的 init 函数也只会执行一次。 其次是同一个包内部的顺序。对于每一个包,初始化顺序是:先初始化常量(const),再初始化变量(var),最后执行 init 函数。

另外,一个包甚至一个文件里可以定义多个 init 函数,它们会按照编译器解析的顺序依次执行。不过在实际开发中,我们通常不建议依赖多个 init 函数的执行顺序,以免造成代码难以维护。”

系统讲解

init 函数是 Go 语言中一个特殊的函数,它没有参数也没有返回值,不能被其他函数显式调用。

核心执行顺序

在 Go 程序的启动阶段,初始化的整体顺序如下:

  1. 解析包依赖:从 main 包开始,按深度优先解析所有被导入的包。
  2. 包级别初始化:对每一个包,按以下顺序执行:
    • 常量初始化 (const)
    • 变量初始化 (var)
    • init() 函数执行
  3. 执行主函数:所有包的 init 函数执行完毕后,最后执行 main 包的 main() 函数。

代码示例

我们可以通过一个简单的例子来验证同一个包内的初始化顺序:

package main
 
import "fmt"
 
const c = "1. const initialized"
 
var v = initVar()
 
func initVar() string {
    fmt.Println("2. var initialized")
    return "var"
}
 
func init() {
    fmt.Println("3. init() executed")
}
 
func main() {
    fmt.Println("4. main() executed")
    fmt.Println(c)
}

输出结果:

2. var initialized
3. init() executed
4. main() executed
1. const initialized

(注:常量 c 的值在编译期就已确定,但在逻辑顺序上它早于变量和 init)

亮点与深度

多个 init 函数的执行顺序

Go 允许在同一个包、甚至同一个文件中定义多个 init 函数。它们的执行顺序规则如下:

  • 不同文件:按照文件名的字典序(如 a.go 先于 b.go)。
  • 同一文件:按照代码中定义的先后顺序,从上到下依次执行。

最佳实践:尽管规则明确,但强烈建议不要在业务逻辑中依赖多个 init 函数的执行顺序。这会导致代码的隐式耦合,极大地增加排查 Bug 的难度。

典型应用场景

init 函数通常用于那些在程序运行前必须就绪的准备工作:

  1. 注册模式:最经典的例子是标准库 database/sql。各个数据库驱动(如 MySQL 驱动)会在自己的 init 函数中调用 sql.Register 注册自己。我们通过 import _ "github.com/go-sql-driver/mysql" 导入包,就是为了触发它的 init 函数。
  2. 环境变量校验:在服务启动前,检查必须的环境变量是否已配置,否则直接 panic
  3. 单例/全局状态初始化:初始化复杂的全局变量、配置对象或连接池。

常见追问

追问:可以在代码里主动调用 init() 函数吗?

不可以。init 函数是由 Go 运行时(Runtime)自动调用的。如果我们在代码中写 init(),编译器会直接报错:undefined: init。它在语言层面上被设计为不可被引用和调用的特殊存在。