Files
cs-note/hzh/GolangStar/Go语言原理/defer原理.md
T

204 lines
6.2 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, defer]
create time: 2026-06-07 15:35
---
# Defer 执行原理
## 概述
本文从编译期和运行期两个层面解析 Go `defer` 的底层实现:_defer 链表结构、三种分配方式(堆/栈/开放编码)、以及 LIFO 执行机制。理解 defer 的原理能帮你避免循环中的性能陷阱,也能更深入地理解 panic/recover 的行为。
> [!question] ❓ 思考
> 为什么多个 defer 按后进先出(LIFO)顺序执行?defer 注册的函数参数是在注册时求值还是执行时求值?Go 1.14 引入的"开放编码"优化了什么?
## 正文
### 一、Defer 的存储结构:_defer 链表
每次使用 `defer` 关键字都会创建一个 `_defer` 结构,同一函数中的多个 defer 通过链表串联:
```go
type _defer struct {
started bool // 是否已开始执行
heap bool // 是否分配在堆上
openDefer bool // 是否使用开放编码模式
sp uintptr // 调用方栈指针
pc uintptr // 调用方程序计数器
fn func() // 延迟执行的函数(注意:已经是 bound 过的)
_panic *_panic // 关联的 panic 对象
link *_defer // 链表下一个节点
fd unsafe.Pointer // defer 相关参数
}
```
存储示意:
```mermaid
flowchart LR
GP["goroutine._defer"] --> D1["_defer #3<br/>fn: Close()"]
D1 --> D2["_defer #2<br/>fn: Unlock()"]
D2 --> D3["_defer #1<br/>fn: Log()"]
D3 --> nil["nil"]
style GP fill:#e3f2fd
style D1 fill:#ffebee
style D2 fill:#fff9c4
style D3 fill:#e8f5e9
```
每个新的 defer 插入到链表**头部**,执行时也从头部取出——这就是 LIFO 顺序的由来。
### 二、编译期处理:三种实现方式
Go 编译器在 SSA 阶段遇到 `defer` 时,会决定用哪种方式实现:
```mermaid
flowchart TD
CanOpen{"可开放编码?<br/>函数≤8个defer<br/>不在循环中<br/>返回值数×defer数≤15"}
CanOpen -->|是| OpenCoded["开放编码: 内联到每个 exit path"]
CanOpen -->|否| NoEscape{内存逃逸?}
NoEscape -->|否 - 栈上| StackAlloc["栈分配: deferprocStack"]
NoEscape -->|是 - 堆上| HeapAlloc["堆分配: deferproc"]
style OpenCoded fill:#e8f5e9
style StackAlloc fill:#fff9c4
style HeapAlloc fill:#ffebee
```
#### 方式 1:开放编码(Open-Coded Defer)— Go 1.14+
当满足条件时,编译器将 defer 逻辑直接内联到函数的每个 return 路径中,省去了函数调用的开销。
```go
// 原始代码
func f() (int, int) {
defer fmt.Println("first")
defer fmt.Println("second")
return 1, 2
}
// 编译后等效于:
func f() (int, int) {
fmt.Println("second")
fmt.Println("first")
return 1, 2
}
```
适用条件:
- 函数中 defer 数量 ≤ 8
- 不在 for/while 循环体内
- 返回值个数 × defer 个数 ≤ 15
- 未使用 `-N` 编译标志
#### 方式 2:栈分配 — Go 1.13+
当 defer 不逃逸时,_defer 结构直接在函数调用栈上分配:
```go
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false
d.sp = getcallersp()
d.pc = getcallerpc()
// 链入 goroutine 的 _defer 链表头部
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
}
```
栈分配避免了堆上的 malloc/free,性能优于堆分配。
#### 方式 3:堆分配 — 传统方式
当 defer 发生逃逸(如在循环中),_defer 分配在堆上:
```go
func deferproc(fn func()) {
gp := getg()
d := newdefer() // 从 P 或全局 deferpool 获取,无则 mallocgc
d.link = gp._defer
gp._defer = d
d.fn = fn
}
```
> [!warning] ⚠️ 重要
> **不要在循环中使用 defer!** 循环中的 defer 无法使用开放编码,且必定逃逸到堆上,导致每次迭代都触发 malloc/free。如果需要清理资源,手动在循环内调用 cleanup 函数。
### 三、运行期执行:deferreturn
函数返回前,编译器插入 `deferreturn()` 调用:
```go
func deferreturn() {
gp := getg()
for {
d := gp._defer
if d == nil { return }
if d.sp != getcallersp() { return } // 跨帧保护
if d.openDefer {
runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
fn := d.fn // 取出函数指针
d.fn = nil // 置空
gp._defer = d.link // 移到下一个
freedefer(d) // 释放(优先归还到 deferpool)
fn() // 执行
}
}
```
执行流程:
```mermaid
flowchart TD
F["函数 return"] --> DR["deferreturn()"]
DR --> Loop{"有 _defer?"}
Loop -->|否| Ret["正常返回"]
Loop -->|是| Next["取头部 _defer"]
Next --> Exec{"开放编码?"}
Exec -->|是| RunOpen["runOpenDeferFrame"]
Exec -->|否| Call["fn() 调用函数"]
RunOpen --> Free["freedefer 释放"]
Call --> Free
Free --> Loop
style DR fill:#e3f2fd
style Call fill:#fff9c4
style Ret fill:#e8f5e9
```
### 四、DeferPool:内存复用
`newdefer` 不会每次都分配新内存,而是采用三层缓存策略:
```
P.local deferpool → sched.global deferpool → heap malloc
```
1. 先从当前 P 的本地 deferpool 中取
2. 如果本地为空且全局池非空,批量从全局池迁移到本地
3. 如果都没有,才在堆上 `new(_defer)`
执行完毕后,`freedefer` 会将 _defer 放回 P 的本地 deferpool 复用。
> [!tip] 💡 理解要点
> 这个设计类似于 TCMalloc 的 ThreadCache 思想——通过本地缓存减少锁竞争和 GC 压力。
## 小结
- Defer 用 _defer 链表实现,新节点插入头部 → LIFO 执行顺序
- 三种实现方式:开放编码(最快)> 栈分配 > 堆分配(最慢)
- 循环中用 defer 会导致逃逸到堆,应避免
- defer 注册的函数和参数在**注册时即已确定**(包括命名返回值的快照)
## 关联笔记
- [[hzh/GolangStar/Go语言基础/Go语言defer]] — Defer 的基础用法
- [[hzh/GolangStar/Go语言基础/Go语言异常捕获]] — Panic/Recover 与 Defer 的关系
- [[hzh/GolangStar/Go语言原理/逃逸分析]] — 变量何时逃逸到堆