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