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 函数的执行顺序?
答案:
init 在 main 之前执行。完整顺序:
- 按依赖图深度优先初始化所有导入包
- 每个包先初始化包级变量,再执行 init
- 所有包初始化完成后执行
main
Q2: 同一个包可以有多个 init 函数吗?
答案:
可以。同一个文件可以有多个 init,同一个包的不同文件也可以有各自的 init。同文件内按声明顺序执行,不同文件按文件名字母顺序执行(但不要依赖文件名顺序)。
Q3: init 中可以使用包级变量吗?
答案:
可以。包级变量在 init 之前初始化,所以 init 中可以安全使用包级变量。但要注意变量的初始化顺序——按声明顺序,如果变量 B 依赖变量 A,A 必须声明在 B 之前。