Files
cs-note/hzh/GolangStar/Go语言原理/程序初始化.md
T

163 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go-principle, initialization]
create time: 2026-06-07 15:05
---
# 程序初始化
## 概述
本文从 runtime 源码角度解析 Go 程序的启动过程:包如何导入、变量如何初始化、`init()` 函数按什么顺序执行。理解这些机制,能帮助你在复杂项目中正确组织初始化逻辑,避免隐式的依赖陷阱。
> [!question] ❓ 思考
> 如果一个包的 `init()` 函数里调用了另一个包的变量,而这个变量依赖于当前包,会发生什么?Go 是如何保证初始化顺序正确的?
## 正文
### 一、初始化流程全景图
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
```
一句话总结:**导入自底向上,初始化也是自底向上,最后才跑 main 函数。**
### 二、包级初始化的顺序规则
#### 规则清单
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)
```
输出顺序为:
```
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 ← 终于到了!
```
### 三、Runtime 视角:init 函数从哪里来
在编译阶段,编译器会将每个 `.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