推荐阅读:runtime/panic.go、runtime/panic.go - defer
概述
defer、panic、recover 是 Go 提供的延迟执行与异常恢复机制:
- defer:在当前函数返回前按 LIFO 顺序执行注册的函数,常用于清理资源;panic 时也会执行。
- panic:触发运行时异常,中断当前函数执行,转而去执行已注册的 defer,若未被 recover 则程序崩溃。
- recover:仅在 defer 所调用的函数体内 生效,用于恢复当前 goroutine 的 panic,使程序继续执行。
三者配合:panic 后只有 defer 会运行,因此 recover 必须写在 defer 里才能“接住” panic。
defer
基本用法与语义
1 | func example() { |
- 编译器将
defer f(args)转为:注册阶段调用deferproc,函数返回前插入deferreturn。 - G 持有 defer 链表,新 defer 插在链表头,执行时从链表头取,实现 LIFO。
编译转换
Go 编译器会将 defer 语句转换为:defer 注册时调用 deferproc,函数返回处插入 deferreturn。返回值大于 0 表示发生 panic,用于 recover 机制。
编译器转换示例(概念)
1 | // 用户代码 |
defer 链表结构
G 持有 defer 链表,新注册的 defer 插入链表头,执行时从链表头取,实现 LIFO。
核心结构体
1 | type funcval struct { |
注册:deferproc / deferprocStack
1 | // deferproc 由编译器在遇到 defer 语句时插入调用;siz 为参数+返回值字节数,argp 指向调用者栈上参数区 |
执行:deferreturn
1 | // deferreturn 由编译器在函数每个返回路径前插入;只处理 d.sp == getcallersp() 的 defer |
闭包处理
若注册的函数有捕获列表(闭包),会创建闭包对象,将捕获变量堆分配并存入闭包结构。
执行完成判断
当前函数是否执行完所有 defer:看 defer 链表头节点的 sp 是否等于当前函数栈指针。
函数栈帧变化
- 栈上 defer(Go 1.13+):
_defer与参数空间在栈上,紧跟在局部变量后;G._defer 通过 link 指向栈上的 _defer。 - 堆上 defer(Go 1.12 及之前):
_defer在堆上,参数需从栈拷到堆,执行时再拷回栈。 - 栈指针 sp:用于标识 defer 所属函数栈帧;deferreturn 只执行
d.sp == getcallersp()的 defer。
栈上 defer 创建时的栈帧结构(Go 1.13+)
graph TB
subgraph "调用者栈帧"
A1[调用者局部变量]
A2[返回地址]
end
subgraph "当前函数栈帧"
B1[函数参数]
B2[局部变量]
B3[返回地址]
B4["_defer 结构体 (栈上)"]
B5["参数/返回值空间 (紧跟在 _defer 后)"]
end
subgraph "G 结构体"
C1["defer 链表头 (指向栈上的 _defer)"]
end
A2 --> B1
B3 --> A2
C1 -.->|link| B4
B4 --> B5
栈指针(sp)的作用
graph LR
subgraph "函数 A 栈帧 (sp_A)"
A1[局部变量]
A2["_defer_A (sp = sp_A)"]
end
subgraph "函数 B 栈帧 (sp_B)"
B1[局部变量]
B2["_defer_B (sp = sp_B)"]
end
subgraph "G 结构体"
C1[defer 链表]
end
C1 --> A2
A2 -->|link| B2
执行流程:函数 A 注册 _defer_A(sp = sp_A),再调用函数 B;B 注册 _defer_B(sp = sp_B)。B 返回时只执行 sp 匹配的 _defer_B;A 返回时执行 _defer_A。
栈帧内存布局对比:堆分配 vs 栈分配
Go 1.12 及之前(堆分配):_defer 在堆上,参数需栈→堆→栈两次拷贝;G._defer 指向堆上的 _defer。
Go 1.13+(栈分配):非循环、可静态确定的 defer 在栈上分配 _defer,参数紧跟其后,无拷贝;函数返回时栈帧(含 _defer)自动回收。
graph TB
subgraph "Go 1.12 堆分配"
H1["栈上参数"] -->|拷贝到堆| H2["堆上 _defer + 参数"]
H2 -->|执行时拷贝回栈| H3[执行 defer 函数]
end
subgraph "Go 1.13+ 栈分配"
S1["栈上 _defer + 参数"] -->|直接读取| S2[执行 defer 函数]
end
函数返回时栈帧的变化
函数返回前执行 defer 时,栈帧的变化过程:
sequenceDiagram
participant 调用者栈帧
participant 当前函数栈帧
participant deferreturn
participant 栈上_defer
participant defer函数
Note over 当前函数栈帧: 1. 函数执行完毕,准备返回
当前函数栈帧->>deferreturn: 调用 deferreturn
deferreturn->>栈上_defer: 读取 _defer 结构(通过 sp 定位)
Note over 栈上_defer: 2. 从栈上读取参数
栈上_defer->>当前函数栈帧: 拷贝参数到调用者栈帧
Note over 当前函数栈帧: 3. 参数已拷贝到栈帧
deferreturn->>defer函数: 调用 defer 函数
defer函数->>调用者栈帧: 使用拷贝的参数执行
Note over 调用者栈帧: 4. defer 执行完成
deferreturn->>当前函数栈帧: 清理栈上 _defer
Note over 当前函数栈帧: 5. 栈帧回收,函数返回
执行阶段的栈帧变化(步骤)
graph TB
subgraph "步骤 1: 函数准备返回"
A1["当前函数栈帧
sp = sp_current"]
A2["栈上 _defer
sp = sp_current
参数空间: [arg1, arg2]"]
A3["deferreturn 检查
sp == _defer.sp?"]
A1 --> A2
A2 --> A3
end
subgraph "步骤 2: 拷贝参数到调用者栈帧"
B1["调用者栈帧
(函数返回后的栈帧)"]
B2["参数空间
[arg1, arg2]
(从 _defer 拷贝)"]
A2 -->|拷贝参数| B2
B1 --> B2
end
subgraph "步骤 3: 执行 defer 函数"
C1["defer 函数栈帧"]
C2["使用拷贝的参数
执行函数体"]
B2 --> C1
C1 --> C2
end
subgraph "步骤 4: 清理栈帧"
D1["回收 _defer
(栈上自动回收)"]
C2 --> D1
end
说明:参数从 _defer 后拷贝到调用者栈帧对应位置,再通过 _defer.fn 调用 defer 函数;执行完后栈帧(含栈上 _defer)自动回收。
注册阶段与执行阶段
- 注册:defer 语句执行时触发
deferproc,参数拷到_defer后,插入G._defer链表头。 - 执行:函数返回前(正常 return 或 panic)执行
deferreturn,按 sp 取当前函数的 defer,拷贝参数、调用fn、回收;jmpdefer使返回时再次回到deferreturn继续下一个。
freedefer 与 defer 池
堆上分配的 _defer 执行完后归还到 P 的 deferpool[siz],按 siz 分桶复用。
1 | // freedefer 将堆上分配的 _defer 归还到 P 的池中 |
流程图(defer)
sequenceDiagram
participant 用户代码
participant deferproc
participant G结构体
participant _defer对象
participant deferreturn
participant 缓存池
用户代码->>deferproc: 执行 defer 语句(注册)
deferproc->>_defer对象: 创建 _defer、拷贝参数
_defer对象->>G结构体: 挂载至 defer 链表头
用户代码->>deferreturn: 函数返回前(return/panic)
deferreturn->>G结构体: 取链表头 _defer
deferreturn->>_defer对象: 拷贝参数至栈、执行注册函数
deferreturn->>缓存池: 回收 _defer 对象
defer 状态图
stateDiagram-v2
[*] --> 注册defer
注册defer --> defer链表
defer链表 --> 函数即将返回
函数即将返回 --> 执行defer
执行defer --> 已回收
执行defer --> 发生panic
发生panic --> recover判断
recover判断 --> 恢复正常流程
recover判断 --> 未恢复
恢复正常流程 --> 已回收
未恢复 --> 程序崩溃
已回收 --> [*]
程序崩溃 --> [*]
优化演进
- Go 1.12 及之前:所有 defer 堆分配,参数栈→堆→栈两次拷贝。
- Go 1.13:非循环、可静态确定的 defer 在栈上分配
_defer,参数在栈上,无拷贝;heap字段区分栈/堆。 - Go 1.14:开放编码(open-coded)defer,≤8 个且非循环时在函数返回前直接内联执行,不创建 _defer;panic 时通过栈上 framepc/varp/fd 查找并执行未执行的开放编码 defer。
开放编码 _defer 扩展字段(与前述 _defer 可合并为同一结构):
1 | type _defer struct { |
- 正常返回:编译器在返回前生成代码,根据
fd指向的 deferBits 逐位检查,若为 1 则执行对应序号的 defer 并清零该位。 - panic 处理:开放编码的 defer 未全部执行时,需通过栈扫描(varp、framepc、fd)找到未执行的开放编码 defer 并执行,再执行链表上的传统 defer;recover 语义不变。
小结(defer)
- 注册:
deferproc/deferprocStack将参数拷到_defer后插入G._defer链表头。 - 执行:
deferreturn按 sp 只处理当前函数的 defer,拷贝参数、调用fn、回收;jmpdefer实现循环。 - 栈上 defer 无堆分配;Go 1.14+ 开放编码 defer 不创建 _defer,在返回前直接内联执行(≤8 个、非循环)。避免循环里 defer;单函数 defer 数量尽量 ≤8 以利用开放编码。
panic
基本用法与语义
1 | func example() { |
- panic 会立即停止当前函数执行,将新的
_panic头插到 G 的 panic 链表,然后沿 defer 链表执行(LIFO)。 - 若某层 defer 里调用了
recover()并返回非 nil,则当前 panic 被恢复,程序从“正常流程”继续;否则一直向上直到崩溃。
panic 结构体与链表
1 | type _panic struct { |
- 新 panic 头插,所以链表头是最新 panic;输出堆栈时从尾到头(从早到晚)。
panic 链表结构图
graph LR
G[G结构体
_panic指针] -->|指向| P1[panic1
最新panic]
P1 -->|link| P2[panic2]
P2 -->|link| P3[panic3
最早panic]
P3 -->|link| nil[NULL]
说明:新 panic 头插,链表头是最新 panic,链表尾是最早 panic;打印堆栈时从尾到头(从早到晚)。
执行流程概览
flowchart TD
A[panic 发生] --> B[gopanic: 创建 _panic,头插 G._panic]
B --> C[遍历 G._defer 执行 defer]
C --> D{defer 中调用了 recover?}
D -->|是| E[标记 recovered,从 panic 链移除,跳转恢复]
D -->|否| F{defer 中又 panic?}
F -->|是| G[新 panic 头插,旧 panic 标 aborted,继续执行 defer]
F -->|否| H[移除当前 defer,执行下一个]
H --> C
G --> C
E --> I[程序继续]
C -->|defer 耗尽且未恢复| J[fatalf 崩溃]
panic 执行时序图
sequenceDiagram
participant 用户代码
participant gopanic
participant panic链表
participant defer链表
participant defer函数
用户代码->>gopanic: 发生 panic
gopanic->>panic链表: 创建 _panic 结构,头插法插入
gopanic->>defer链表: 遍历 defer 链表(LIFO)
loop 执行每个 defer
defer链表->>defer函数: 执行 defer 函数
alt defer 正常执行
defer函数-->>defer链表: 执行完成,移除 defer
else defer 中发生 panic
defer函数->>gopanic: 触发新 panic
gopanic->>panic链表: 新 panic 插入链表头
gopanic->>panic链表: 标记旧 panic 为 aborted
defer链表->>defer链表: 移除当前 defer
end
end
alt 所有 defer 执行完毕且未恢复
gopanic->>用户代码: 程序崩溃,输出堆栈
end
panic 状态图
stateDiagram-v2
[*] --> 正常执行
正常执行 --> 发生panic: panic()调用
发生panic --> 创建panic结构: gopanic
创建panic结构 --> 执行defer: 遍历defer链表
执行defer --> defer正常: defer执行完成
执行defer --> defer中panic: defer中发生panic
defer正常 --> 继续执行defer: 还有defer
继续执行defer --> 执行defer
defer正常 --> 程序崩溃: 无recover
defer中panic --> 新panic成为当前: 插入链表头
新panic成为当前 --> 标记旧panic终止: aborted=true
标记旧panic终止 --> 执行defer: 继续执行
执行defer --> recover处理: recover()调用
recover处理 --> 标记恢复: recovered=true
标记恢复 --> 删除panic: 从链表移除
删除panic --> 恢复执行: 跳转到正常流程
恢复执行 --> 正常执行
程序崩溃 --> [*]
核心逻辑:gopanic
1 | func gopanic(e interface{}) { |
- 每次执行 defer 前把当前
_panic挂到d._panic,defer 内若调用recover(),会设置p.recovered = true并返回 panic 值。 - 若
p.recovered为 true,则从链表摘下当前 panic,调用gorecover按保存的 sp/pc 跳转,恢复正常执行流。
defer 中再次 panic
- 若在执行某个 defer 时又发生 panic,会再次进入
gopanic,新_panic头插。 - 当前正在执行的 defer 对应的旧 panic 会被标记为
aborted,该 defer 被移除,继续用新 panic 执行剩余 defer。
panic 信息输出
- 输出顺序:打印 panic 信息时从 panic 链表尾开始输出(即按 panic 发生顺序,从最早到最新)。
- 恢复标记:若 panic 已被 recover,输出时会加上
[recovered]标记。
1 | // 输出示例:defer 中先 recover 再 panic |
recover
基本用法与语义
1 | func example() { |
recover()只有在 “被 defer 的函数”体内直接调用 时才会返回当前 panic 的值,否则返回nil。
为什么 recover 只能在 defer 中执行
- 执行顺序:panic 后当前函数后续代码都不会执行,只有已注册的 defer 会被执行。因此能观察到“正在 panic”且还会运行的代码只有 defer。
- 语义:运行时只在“从 panic 流程中执行 defer 函数”的上下文中,让
recover()返回当前 G 的 panic 值;其它路径一律返回nil。
所以必须把 recover() 写在 defer 所调用的那个函数 的函数体里。
时间顺序
flowchart TD
A[正常执行] --> B[某行发生 panic]
B --> C[当前函数内剩余代码被跳过]
C --> D[开始执行当前函数的 defer]
D --> E{某个 defer 里调用 recover()}
E -->|是| F[捕获当前 panic,程序可恢复]
E -->|否| G[继续向上执行上一层的 defer,直至崩溃]
简单对比:defer 里 vs 普通代码里
1 | // 无效:普通代码里调用 recover,无法捕获任何 panic |
为什么 defer recover() 和 defer func() { func() { recover() }() }() 无效
规范要求:recover 只有在“被某个 deferred 函数直接调用”时才会返回当前 panic 的值。“deferred 函数”指你通过 defer 注册的那个函数(如 defer func() { ... }() 里的匿名函数),而不是该函数内部调用的内置函数。
- defer recover():被 defer 的是对
recover的这一句调用本身,没有“另一个 deferred 函数在体内调用 recover”,不满足“被 deferred 函数直接调用”,recover()返回nil。 - defer func() { func() { recover() }() }():被 defer 的是外层匿名函数,
recover()是在内层匿名函数里调用的,不是被 defer 的外层函数直接调用,同样返回nil。中间多包一层函数调用就不算“直接调用”。
对比示例
1 | // 无效:defer 的是 recover 这一句调用本身 |
| 写法 | 谁直接调用了 recover | 是否生效 |
|---|---|---|
defer recover() |
无 | 否 |
defer func() { func() { recover() }() }() |
内层函数 | 否 |
defer func() { recover() }() |
被 defer 的匿名函数 | 是 |
核心逻辑:gorecover
1 | func gorecover(argp uintptr) interface{} { |
- 仅在当前 G 有 panic、且该 panic 未被恢复、且
argp与创建该 panic 时的 argp 一致(即确实是在“对应”的 defer 调用栈中)时,标记recovered并返回p.arg;否则返回nil。 gopanic在每次执行 defer 后检查p.recovered,若为 true 则摘下该 panic 并调用gorecover做栈恢复,程序从恢复点继续执行。
recover 机制时序图
sequenceDiagram
participant defer函数
participant recover
participant panic链表
participant gopanic
participant 用户代码
defer函数->>recover: 调用 recover()
recover->>panic链表: 检查当前 panic
alt 存在 panic
recover->>panic链表: 设置 recovered=true
recover-->>defer函数: 返回 panic 值
defer函数-->>gopanic: defer 执行完成
gopanic->>panic链表: 检测到 recovered=true
gopanic->>panic链表: 删除当前 panic
gopanic->>gopanic: 记录 sp 和 pc
gopanic->>用户代码: 跳转到正常执行流程
else 无 panic
recover-->>defer函数: 返回 nil
end
recover 流程简图
flowchart TD
A[defer 函数执行] --> B{调用 recover?}
B -->|是| C{存在 panic?}
B -->|否| D[继续执行]
C -->|是| E[标记 recovered=true]
C -->|否| F[返回 nil]
E --> G[返回 panic 值]
G --> H[defer 执行完成]
H --> I[检测 recovered]
I --> J[删除 panic]
J --> K[记录 sp 和 pc]
K --> L[跳转到正常流程]
F --> D
D --> M[继续下一个 defer]
recover 跳转:sp 与 pc
recover 后需要跳回“正常流程”,依赖 defer 记录的两个值:
- sp:调用 defer 函数的调用者的栈指针,用于恢复调用者的栈帧。
- pc:defer 注册时
deferproc的返回地址,用于跳转到“defer 注册之后的下一句代码”,即 panic 发生前的正常执行点。
因此“恢复执行”的本质是:从 panic/defer 执行路径跳回原函数的某条指令(pc),并恢复对应栈帧(sp)。
归纳:recover 只能在 defer 中执行 = (1)执行顺序:panic 后只有 defer 会运行;(2)语义与实现:recover() 只有在“当前 G 正在 panic 且正在执行某个 deferred 函数的函数体”的上下文中、且由该函数体直接调用时,才会返回该 panic 的值;其它情况(包括 defer recover()、多包一层函数)一律返回 nil。
recover 后又 panic
- 若在某个 defer 里先
recover()再panic(...),等同于“defer 中发生 panic”:当前 defer 会被移除,新 panic 头插,继续执行剩余 defer。
三者联动与总结
联动关系
- 正常返回:函数返回前执行
deferreturn,按 LIFO 执行当前函数的 defer,不涉及 panic/recover。 - panic:
gopanic创建_panic头插,然后按 G._defer 依次执行 defer;若某 defer 内直接调用recover(),则gorecover标记恢复并返回 panic 值,gopanic跳转恢复点,程序继续;否则 defer 耗尽后fatalpanic崩溃。 - defer 中再 panic:新 panic 头插,旧 panic 标
aborted,继续用新 panic 执行剩余 defer。 - 恢复后执行位置:recover 成功后,程序从“发生 panic 的函数的调用者”继续执行(即带 recover 的 defer 返回后,控制流回到调用者),而不是从 panic 所在行继续执行。
最佳实践
- recover 只写在 defer 里:且必须是被 defer 的那个函数的函数体里直接调用
recover(),不要defer recover()或再多包一层函数。 - 合理使用 panic:用于不可恢复的错误;可恢复的错误建议用
error返回;避免滥用 panic。 - defer 尽量简单:避免在 defer 里做重逻辑或再次 panic,以免掩盖问题。
- 避免循环里 defer:若需在循环中延迟逻辑,可提取到单独函数再 defer。
- 及时处理 panic:在可能发生 panic 的边界(如 HTTP handler、goroutine 入口)使用 defer + recover。
- 记录错误信息:在 recover 中记录 panic 值或堆栈,便于排查问题。
- 单函数 defer 数量:尽量 ≤8 个,以利用 Go 1.14+ 的开放编码优化。
注意事项
- panic/recover 只影响当前 goroutine,不能跨 goroutine 恢复。
- 未恢复的 panic 会导致程序崩溃并打印堆栈。
- 开放编码 defer(Go 1.14+)在 panic 时需通过栈扫描执行未执行的开放编码 defer,再执行链表上的 defer,recover 的语义不变。