vault backup: 2026-06-07 12:14:39
This commit is contained in:
+137
-107
@@ -1,132 +1,162 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go原理深入
|
||||
- 程序初始化
|
||||
tags: [go, golang, go-principle, initialization]
|
||||
create time: 2026-06-07 15:05
|
||||
---
|
||||
|
||||
# 程序初始化
|
||||
|
||||
Go应用程序的初始化是在单一的`goroutine`中执行的。对于包这一级别的初始化来说,在一个包里会先进行包级别变量的初始化。一个包下可以有多个`init`函数,每个文件也可以有多个`init` 函数,多个 `init` 函数按照它们的文件名顺序逐个初始化。但是程序不可能把所有代码都放在一个包里,通常都是会引入很多包。如果`main`包引入了`pkg1`包,`pkg1`包本身又导入了包`pkg2`,那么应用程序的初始化会按照什么顺序来执行呢?
|
||||
## 概述
|
||||
|
||||
对于这个初始化过程我粗略的画了一个示意图,理解起来更直观些。
|
||||
本文从 runtime 源码角度解析 Go 程序的启动过程:包如何导入、变量如何初始化、`init()` 函数按什么顺序执行。理解这些机制,能帮助你在复杂项目中正确组织初始化逻辑,避免隐式的依赖陷阱。
|
||||
|
||||

|
||||
> [!question] ❓ 思考
|
||||
> 如果一个包的 `init()` 函数里调用了另一个包的变量,而这个变量依赖于当前包,会发生什么?Go 是如何保证初始化顺序正确的?
|
||||
|
||||
图的上半部分表示了`main`包导入了`pkg1`包,`pkg1`包又导入了`pkg2`包这样一个包之间的依赖关系。图的下半部分表示了,这个应用初始化工作的执行时间顺序是从被导入的最深层包开始进行初始化,层层递出最后到`main`包,每个包内部的初始化程序依然是先执行包变量初始化再进行`init`函数的执行。
|
||||
## 正文
|
||||
|
||||
下面通过示例来验证一下这个初始化顺序,在`go_tour`目录下有三个包`package1`和`package2`和`utils`,代码目录如下:
|
||||
### 一、初始化流程全景图
|
||||
|
||||
```
|
||||
├─package1
|
||||
├─package2
|
||||
└─utils
|
||||
Go 程序的初始化在**单一 goroutine** 中完成,遵循"先被依赖的包先初始化"的原则。整个过程可以概括为:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph ImportPhase["导入阶段"]
|
||||
A["最深层依赖包<br/>import"] --> B["上层依赖包<br/>import"]
|
||||
B --> C["main 包<br/>import"]
|
||||
end
|
||||
|
||||
subgraph InitPhase["初始化阶段"]
|
||||
D["最深层包:<br/>const → var → init()"] --> E["中间层包:<br/>const → var → init()"]
|
||||
E --> F["main 包:<br/>const → var → init()"]
|
||||
end
|
||||
|
||||
subgraph RunPhase["运行阶段"]
|
||||
G["main.main()"]
|
||||
end
|
||||
|
||||
ImportPhase --> InitPhase --> RunPhase
|
||||
style A fill:#e8f5e9
|
||||
style D fill:#fff9c4
|
||||
style G fill:#ffebee
|
||||
```
|
||||
|
||||
分别定义测试函数,在`utils`包下有文件`utils.go`,在`package1`包下有文件`package1.go`,在`package2`包下有文件`package2.go`。
|
||||
一句话总结:**导入自底向上,初始化也是自底向上,最后才跑 main 函数。**
|
||||
|
||||
```go
|
||||
package utils
|
||||
### 二、包级初始化的顺序规则
|
||||
|
||||
import "fmt"
|
||||
#### 规则清单
|
||||
|
||||
func TraceLog(t string, v int) int {
|
||||
fmt.Printf("TraceLog-----%s--------%d\n", t, v)
|
||||
return v
|
||||
}
|
||||
1. **常量先于变量**:每个包内,`const` 声明最早求值
|
||||
2. **变量按声明顺序求值**:同一文件内的变量从上到下依次初始化
|
||||
3. **多文件的排序**:按文件名字母序执行(由编译器决定)
|
||||
4. **`init()` 函数**:在每个包的变量初始化完成后执行
|
||||
5. **多个 `init()`**:一个包可以有任意多个 `init()` 函数,按文件名字母序逐个调用
|
||||
6. **幂等性**:不管包被导入多少次,其 `init()` 只执行一次
|
||||
7. **main 最后**:所有依赖包初始化完毕后,才执行 `main.main()`
|
||||
|
||||
#### 示例验证
|
||||
|
||||
假设有以下包依赖关系:
|
||||
|
||||
```
|
||||
main 包
|
||||
├── package1 包 (package1.go)
|
||||
│ └── package2 包 (package2.go)
|
||||
│ └── utils 包 (utils.go)
|
||||
└── utils 包 (utils.go)
|
||||
```
|
||||
|
||||
package1包下有如下程序package1.go:
|
||||
|
||||
```go
|
||||
package package1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go_tour/package2"
|
||||
"go_tour/utils"
|
||||
)
|
||||
|
||||
var V1 = utils.TraceLog("init package1 value1", package2.Value1+10)
|
||||
var V2 = utils.TraceLog("init package1 value2", package2.Value2+10)
|
||||
|
||||
func init() {
|
||||
fmt.Println("init func in package1")
|
||||
}
|
||||
```
|
||||
|
||||
package2包下有如下程序package2.go:
|
||||
|
||||
```go
|
||||
package package2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go_tour/utils"
|
||||
)
|
||||
|
||||
var Value1 = utils.TraceLog("init package2 value1", 20)
|
||||
var Value2 = utils.TraceLog("init package2 value2", 30)
|
||||
|
||||
func init() {
|
||||
fmt.Println("init func1 in package2")
|
||||
}
|
||||
|
||||
func init() {
|
||||
fmt.Println("init func2 in package2")
|
||||
}
|
||||
```
|
||||
|
||||
主程序`main.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go_tour/package1"
|
||||
"go_tour/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmt.Println("init func1 in main")
|
||||
}
|
||||
|
||||
func init() {
|
||||
fmt.Println("init func2 in main")
|
||||
}
|
||||
|
||||
var MainValue1 = utils.TraceLog("init M_v1", package1.V1+10)
|
||||
var MainValue2 = utils.TraceLog("init M_v2", package1.V2+10)
|
||||
|
||||
func main() {
|
||||
fmt.Println("main func in main")
|
||||
}
|
||||
```
|
||||
|
||||
执行`go run main.go`,输出结果如下:
|
||||
输出顺序为:
|
||||
|
||||
```
|
||||
TraceLog-----init package2 value1--------20
|
||||
TraceLog-----init package2 value2--------30
|
||||
init func1 in package2
|
||||
init func2 in package2
|
||||
init func1 in package2 ← package2 的 init()
|
||||
init func2 in package2 ← package2 的第二个 init()
|
||||
TraceLog-----init package1 value1--------30
|
||||
TraceLog-----init package1 value2--------40
|
||||
init func in package1
|
||||
TraceLog-----init M_v1--------40
|
||||
TraceLog-----init M_v2--------50
|
||||
init func1 in main
|
||||
init func2 in main
|
||||
main func in main
|
||||
```
|
||||
init func in package1 ← package1 的 init()
|
||||
TraceLog-----init M_v1---------40
|
||||
TraceLog-----init M_v2---------50
|
||||
init func1 in main ← main 的 init()
|
||||
init func2 in main ← main 的第二个 init()
|
||||
main func in main ← 终于到了!
|
||||
```
|
||||
|
||||
实验与结论相符合,按照i导入包的层次,最先被依赖的包最先被初始化,且初始化的顺序是先初始化包变量,再说初始化`init`函数。初始化过程总结如下:
|
||||
### 三、Runtime 视角:init 函数从哪里来
|
||||
|
||||
- **包级别变量的初始化先于包内`init`函数的执行。**
|
||||
- **一个包下可以有多个`init`函数,每个文件也可以有多个`init` 函数。**
|
||||
- **多个 `init` 函数按照它们的文件名顺序逐个初始化。**
|
||||
- **应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到main包。**
|
||||
- **不管包被导入多少次,包内的`init`函数只会执行一次。**
|
||||
- **应用在所有初始化工作完成后才会执行`main`函数。**
|
||||
在编译阶段,编译器会将每个 `.go` 文件中声明的 `init()` 函数收集起来,合并成一个内部的 `init_main` 函数。具体过程位于 `cmd/compile/internal/noder/initOrder.go`:
|
||||
|
||||
```go
|
||||
// 编译器内部逻辑(简化)
|
||||
func (n *node) initOrder() []ir.Node {
|
||||
// 1. 遍历所有包的所有文件
|
||||
// 2. 收集 init() 函数,按包依赖拓扑排序
|
||||
// 3. 同一文件内的 init() 按声明顺序排列
|
||||
return sortedInits
|
||||
}
|
||||
```
|
||||
|
||||
运行时,runtime 在 `runtime/proc.go` 中的 `runInit` 函数会依次调用这些初始化函数:
|
||||
|
||||
```go
|
||||
// src/runtime/proc.go (简化)
|
||||
func runInit(inits []func()) {
|
||||
for _, fn := range inits {
|
||||
fn() // 逐个执行 init 函数
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 📝 源码要点
|
||||
> 初始化顺序是**编译期确定**的,不是运行期动态计算。这保证了行为的可预测性——同一个项目在多次运行中初始化顺序完全一致。
|
||||
|
||||
### 四、常见陷阱
|
||||
|
||||
#### 陷阱 1:循环依赖
|
||||
|
||||
```go
|
||||
// 包 A 导入了包 B,包 B 又导入了包 A
|
||||
// 编译失败:import cycle not allowed
|
||||
```
|
||||
|
||||
Go 不允许包之间形成环,这是初始化安全的第一道防线。
|
||||
|
||||
#### 陷阱 2:变量相互引用
|
||||
|
||||
```go
|
||||
var A = B + 1 // B 还未初始化,值为 0
|
||||
var B = A + 1 // A 已初始化为 1,所以 B = 2, A = 2
|
||||
```
|
||||
|
||||
变量按声明顺序依次求值,而非同时。这个行为容易引发微妙的 bug。
|
||||
|
||||
#### 陷阱 3:init 中的 panic
|
||||
|
||||
如果某个包的 `init()` 函数 panic 了,整个程序会在启动阶段崩溃,且错误信息不够友好。建议:
|
||||
|
||||
> [!warning] ⚠️ 注意
|
||||
> `init()` 中不要做复杂逻辑或 I/O 操作。如果有需要,考虑用显式的 `NewXXX()` 构造函数替代。
|
||||
|
||||
### 五、初始化与并发的关系
|
||||
|
||||
由于初始化在单一 goroutine 中完成,**初始化期间不会有任何并发竞争**。这意味着:
|
||||
|
||||
- `init()` 中可以安全地使用全局变量
|
||||
- 不需要 mutex 保护初始化逻辑
|
||||
- 但这也意味着大量初始化逻辑会串行执行,可能成为启动瓶颈
|
||||
|
||||
对于需要快速启动的场景(如 CLI 工具),可以考虑延迟初始化——在真正使用时再初始化资源。
|
||||
|
||||
## 小结
|
||||
|
||||
- Go 初始化遵循严格的拓扑排序:被依赖最深的包最先初始化
|
||||
- 每个包内顺序为:const → var → init()
|
||||
- 初始化在单 goroutine 中完成,天然线程安全
|
||||
- 避免在 `init()` 中做复杂操作,优先使用显式构造函数
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言基础/Go语言代码结构]] — package/import 的基本用法
|
||||
- [[hzh/GolangStar/Go语言进阶/并发概述]] — Goroutine 并发模型
|
||||
- [[hzh/GolangStar/Go语言原理/gmp调度原理]] — GMP 调度器如何管理 goroutine
|
||||
|
||||
Reference in New Issue
Block a user