2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
2026-06-07 12:14:39 +08:00
|
|
|
|
tags: [go, golang, go-principle, initialization]
|
|
|
|
|
|
create time: 2026-06-07 15:05
|
2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 程序初始化
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 概述
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
本文从 runtime 源码角度解析 Go 程序的启动过程:包如何导入、变量如何初始化、`init()` 函数按什么顺序执行。理解这些机制,能帮助你在复杂项目中正确组织初始化逻辑,避免隐式的依赖陷阱。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!question] ❓ 思考
|
|
|
|
|
|
> 如果一个包的 `init()` 函数里调用了另一个包的变量,而这个变量依赖于当前包,会发生什么?Go 是如何保证初始化顺序正确的?
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 正文
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 一、初始化流程全景图
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
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
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
一句话总结:**导入自底向上,初始化也是自底向上,最后才跑 main 函数。**
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 二、包级初始化的顺序规则
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 规则清单
|
|
|
|
|
|
|
|
|
|
|
|
1. **常量先于变量**:每个包内,`const` 声明最早求值
|
|
|
|
|
|
2. **变量按声明顺序求值**:同一文件内的变量从上到下依次初始化
|
|
|
|
|
|
3. **多文件的排序**:按文件名字母序执行(由编译器决定)
|
|
|
|
|
|
4. **`init()` 函数**:在每个包的变量初始化完成后执行
|
|
|
|
|
|
5. **多个 `init()`**:一个包可以有任意多个 `init()` 函数,按文件名字母序逐个调用
|
|
|
|
|
|
6. **幂等性**:不管包被导入多少次,其 `init()` 只执行一次
|
|
|
|
|
|
7. **main 最后**:所有依赖包初始化完毕后,才执行 `main.main()`
|
|
|
|
|
|
|
|
|
|
|
|
#### 示例验证
|
|
|
|
|
|
|
|
|
|
|
|
假设有以下包依赖关系:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```
|
|
|
|
|
|
main 包
|
|
|
|
|
|
├── package1 包 (package1.go)
|
|
|
|
|
|
│ └── package2 包 (package2.go)
|
|
|
|
|
|
│ └── utils 包 (utils.go)
|
|
|
|
|
|
└── utils 包 (utils.go)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
输出顺序为:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```
|
|
|
|
|
|
TraceLog-----init package2 value1--------20
|
|
|
|
|
|
TraceLog-----init package2 value2--------30
|
|
|
|
|
|
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 ← 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 ← 终于到了!
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 三、Runtime 视角:init 函数从哪里来
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
在编译阶段,编译器会将每个 `.go` 文件中声明的 `init()` 函数收集起来,合并成一个内部的 `init_main` 函数。具体过程位于 `cmd/compile/internal/noder/initOrder.go`:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```go
|
|
|
|
|
|
// 编译器内部逻辑(简化)
|
|
|
|
|
|
func (n *node) initOrder() []ir.Node {
|
|
|
|
|
|
// 1. 遍历所有包的所有文件
|
|
|
|
|
|
// 2. 收集 init() 函数,按包依赖拓扑排序
|
|
|
|
|
|
// 3. 同一文件内的 init() 按声明顺序排列
|
|
|
|
|
|
return sortedInits
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
运行时,runtime 在 `runtime/proc.go` 中的 `runInit` 函数会依次调用这些初始化函数:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// src/runtime/proc.go (简化)
|
|
|
|
|
|
func runInit(inits []func()) {
|
|
|
|
|
|
for _, fn := range inits {
|
|
|
|
|
|
fn() // 逐个执行 init 函数
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!note] 📝 源码要点
|
|
|
|
|
|
> 初始化顺序是**编译期确定**的,不是运行期动态计算。这保证了行为的可预测性——同一个项目在多次运行中初始化顺序完全一致。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 四、常见陷阱
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 陷阱 1:循环依赖
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```go
|
|
|
|
|
|
// 包 A 导入了包 B,包 B 又导入了包 A
|
|
|
|
|
|
// 编译失败:import cycle not allowed
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
Go 不允许包之间形成环,这是初始化安全的第一道防线。
|
|
|
|
|
|
|
|
|
|
|
|
#### 陷阱 2:变量相互引用
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
var A = B + 1 // B 还未初始化,值为 0
|
|
|
|
|
|
var B = A + 1 // A 已初始化为 1,所以 B = 2, A = 2
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
变量按声明顺序依次求值,而非同时。这个行为容易引发微妙的 bug。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 陷阱 3:init 中的 panic
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
如果某个包的 `init()` 函数 panic 了,整个程序会在启动阶段崩溃,且错误信息不够友好。建议:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ 注意
|
|
|
|
|
|
> `init()` 中不要做复杂逻辑或 I/O 操作。如果有需要,考虑用显式的 `NewXXX()` 构造函数替代。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 五、初始化与并发的关系
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
由于初始化在单一 goroutine 中完成,**初始化期间不会有任何并发竞争**。这意味着:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
- `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
|