跳到主要内容

init 函数与包初始化

问题

Go 的 init 函数有什么特点?包初始化的执行顺序是什么?

答案

init 函数特点

init 是 Go 的特殊函数,在包初始化时自动执行

package main

import "fmt"

func init() {
fmt.Println("init 1")
}

func init() {
fmt.Println("init 2") // 同一个文件可以有多个 init
}

func main() {
fmt.Println("main")
}
// 输出:init 1 → init 2 → main

init 的规则

规则说明
无参数无返回值func init() 是唯一合法签名
不能被调用init() 调用会编译错误
自动执行包被导入时自动执行
可以有多个同一文件、同一包可以有多个 init
执行顺序同文件按声明顺序,同包按文件名字母序

包初始化顺序

完整顺序:导入依赖 → 依赖包变量初始化 → 依赖包 init → 当前包变量初始化 → 当前包 init → main

// pkg/a.go
package pkg

var A = initA() // 2️⃣ 包级变量初始化

func initA() int {
fmt.Println("var A initialized")
return 1
}

func init() {
fmt.Println("pkg init") // 3️⃣ init 执行
}

// main.go
package main

import "pkg" // 1️⃣ 导入包

var M = initM() // 4️⃣ main 包变量初始化

func initM() int {
fmt.Println("var M initialized")
return 1
}

func init() {
fmt.Println("main init") // 5️⃣ main 的 init
}

func main() {
fmt.Println("main") // 6️⃣ main 函数
}
// 输出:var A initialized → pkg init → var M initialized → main init → main

副作用导入(Blank Import)

有时导入包只是为了执行它的 init 函数,不使用包中的任何符号:

import (
// 注册数据库驱动(init 中调用 sql.Register)
_ "github.com/go-sql-driver/mysql"

// 注册图片解码器
_ "image/png"
_ "image/jpeg"

// 加载 pprof HTTP 处理器
_ "net/http/pprof"
)
init 的缺点
  • 隐式副作用:难以追踪哪些代码在 init 中执行
  • 测试困难:init 在 test 前执行,可能影响测试环境
  • 循环依赖风险:多个包的 init 互相依赖可能导致问题
  • 错误处理困难:init 不能返回 error,只能 panic

建议:尽量减少 init 的使用,优先用显式的初始化函数。


常见面试问题

Q1: init 函数和 main 函数的执行顺序?

答案

initmain 之前执行。完整顺序:

  1. 按依赖图深度优先初始化所有导入包
  2. 每个包先初始化包级变量,再执行 init
  3. 所有包初始化完成后执行 main

Q2: 同一个包可以有多个 init 函数吗?

答案

可以。同一个文件可以有多个 init,同一个包的不同文件也可以有各自的 init。同文件内按声明顺序执行,不同文件按文件名字母顺序执行(但不要依赖文件名顺序)。

Q3: init 中可以使用包级变量吗?

答案

可以。包级变量在 init 之前初始化,所以 init 中可以安全使用包级变量。但要注意变量的初始化顺序——按声明顺序,如果变量 B 依赖变量 A,A 必须声明在 B 之前。

相关链接