Go-09 defer、panic 与 recover
1. defer 基础
defer 是 Go 的关键字,用于注册一个「延迟调用」:被 defer 修饰的函数不会立即执行,而是等到外层函数即将返回时才执行。它是 Go 处理资源释放、异常恢复的核心机制。
1.1 延迟执行的语义
package main
import "fmt"
func main() {
fmt.Println("1. 函数开始")
defer fmt.Println("3. defer 执行(函数返回前)")
fmt.Println("2. 函数逻辑")
}
// 输出:
// 1. 函数开始
// 2. 函数逻辑
// 3. defer 执行(函数返回前)
defer 的核心保证:无论函数从哪条路径返回(正常 return、panic、runtime 错误),已注册的 defer 一定会被执行。这使它成为「清理逻辑」的理想载体。
1.2 三大常见用途
用途一:关闭文件 / 释放连接
func readFile(name string) ([]byte, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close() // 紧跟 Open,保证任何返回路径都会关闭
return io.ReadAll(f)
}
defer 让「打开」和「关闭」在代码上紧挨着,避免了在每个 return 前手动 f.Close(),也杜绝了新增分支时漏掉关闭。
用途二:解锁互斥锁
var mu sync.Mutex
func safeUpdate(m map[string]int, k string) {
mu.Lock()
defer mu.Unlock() // 即使中途 panic,锁也会释放,不会死锁
m[k]++
}
用途三:配合 recover 捕获 panic
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("捕获到 panic: %v", r)
}
}()
result = a / b // b==0 时触发 panic,被上面的 defer recover
return
}
1.3 defer 的记账时机
一个关键点:defer 语句在被执行到的那一刻就完成了「注册」(把要延迟的调用压入栈)。如果代码根本没执行到 defer 语句,就不会注册。
func f() {
return // 提前返回
defer fmt.Println("永远不会执行") // 编译器会警告 unreachable
}
2. 执行顺序:LIFO(后进先出)
一个函数中可以有多个 defer,它们的执行顺序是 LIFO(Last In First Out,后进先出),就像一个栈:最后注册的最先执行。
2.1 基础栈式演示
func main() {
for i := 1; i <= 3; i++ {
defer fmt.Println("defer:", i)
}
fmt.Println("main 结束")
}
// 输出:
// main 结束
// defer: 3 ← 最后注册,最先执行
// defer: 2
// defer: 1 ← 最先注册,最后执行
图示这个「压栈 / 出栈」过程:
注册阶段(压栈) 执行阶段(出栈,函数返回时)
┌─────────────┐ ┌─────────────┐
│ defer 3 │ ← top │ defer 3 │ → 先执行
├─────────────┤ ├─────────────┤
│ defer 2 │ │ defer 2 │ → 再执行
├─────────────┤ ├─────────────┤
│ defer 1 │ ← bottom │ defer 1 │ → 最后执行
└─────────────┘ └─────────────┘
2.2 为什么是 LIFO?
LIFO 的设计非常符合资源管理的直觉:后申请的资源应当先释放。
func process() {
conn := openConn()
defer conn.Close() // 3. 最后关连接
tx := conn.Begin()
defer tx.Rollback() // 2. 先回滚事务
lock := acquireLock()
defer lock.Unlock() // 1. 最先释放锁
// ... 业务逻辑 ...
}
// 释放顺序:Unlock → Rollback → Close
// 与获取顺序完全相反,符合嵌套资源的正确释放语义
2.3 嵌套函数中的 defer
defer 只对当前所在的函数生效,不会跨函数。
func inner() {
defer fmt.Println("inner defer")
fmt.Println("inner body")
}
func outer() {
defer fmt.Println("outer defer")
inner()
fmt.Println("outer body")
}
func main() { outer() }
// 输出:
// inner body
// inner defer ← inner 返回时执行
// outer body
// outer defer ← outer 返回时执行
3. defer 参数预计算陷阱
这是 Go 面试中出现频率极高的考点:defer 后面函数的参数,在 defer 语句执行(注册)的那一刻就被求值并保存下来了,而不是等到真正调用时才求值。
3.1 直接传参:立即求值
func main() {
i := 0
defer fmt.Println("defer 时 i =", i) // 此刻 i=0 被复制保存
i = 100
fmt.Println("现在 i =", i)
}
// 输出:
// 现在 i = 100
// defer 时 i = 0 ← 保存的是注册时的快照 0,不是 100
defer fmt.Println("...", i) 在注册时,i 的值 0 就被拷贝进了这次延迟调用的参数里,之后 i 怎么变都与它无关。
3.2 循环中的经典输出题
func main() {
for i := 0; i < 3; i++ {
defer fmt.Print(i, " ") // 每次循环,i 的当前值被立即保存
}
}
// 输出:2 1 0
// 原因:注册时分别保存了 0、1、2;执行时 LIFO 逆序输出
3.3 闭包:延迟求值
如果把变量放进闭包里(而不是作为参数),情况就完全不同——闭包捕获的是变量的引用,执行时才读取其最终值。
func main() {
i := 0
defer func() {
fmt.Println("闭包读取 i =", i) // 执行时才读,读到最终值
}()
i = 100
}
// 输出:闭包读取 i = 100 ← 闭包捕获引用,读到修改后的值
对比两种写法(重要):
func main() {
i := 0
defer fmt.Println("传参:", i) // 快照 → 打印 0
defer func() { fmt.Println("闭包:", i) }() // 引用 → 打印 100
i = 100
}
// 输出(LIFO,闭包后注册先执行):
// 闭包: 100
// 传参: 0
3.4 Go 1.22 循环变量语义变化
Go 1.22 起,for 循环的循环变量每次迭代都是新变量。这只影响「闭包捕获」的场景,不影响本节的「参数预计算」(参数预计算本来就每次保存快照)。
// Go 1.22+
func main() {
for i := 0; i < 3; i++ {
defer func() { fmt.Print(i, " ") }() // 每次 i 是新变量
}
}
// Go 1.22+ 输出:2 1 0(各自捕获独立的 i)
// Go 1.21 及以前 输出:3 3 3(共享同一个 i,循环结束后都为 3)
记忆口诀:参数「拍照存档」,闭包「现场取值」。
4. defer 与命名返回值
defer 能不能修改函数的返回值?答案是:能,但仅当使用命名返回值时。理解这一点必须先搞清楚 return 语句在底层是「两步操作」。
4.1 return 分两步
一条 return x 语句,在编译后实际被拆成两步:
第 1 步:给返回值变量赋值 (ret = x)
第 2 步:执行所有 defer
第 3 步:真正返回给调用方 (RET 指令)
关键在于:defer 在第 1 步和第 3 步之间执行。所以只要 defer 能拿到「返回值变量」并修改它,就能影响最终返回结果。
4.2 命名返回值:可以被修改
// 命名返回值 result:defer 拿得到它、改得了它
func namedReturn() (result int) {
defer func() {
result++ // 修改的就是那个即将返回的变量
}()
result = 10
return result // ① result=10 → ② defer 令 result=11 → ③ 返回 11
}
func main() {
fmt.Println(namedReturn()) // 输出:11
}
4.3 匿名返回值:无法被修改
// 匿名返回值:return 时把 i 拷贝给一个「匿名的返回值变量」
func anonReturn() int {
i := 10
defer func() {
i++ // 改的是局部变量 i,改不到那个匿名返回值变量
}()
return i // ① 匿名返回值 = i = 10 → ② defer 改 i=11(无关)→ ③ 返回 10
}
func main() {
fmt.Println(anonReturn()) // 输出:10
}
用伪代码展开 anonReturn 的 return i,就一目了然:
// return i 被编译器展开为:
retVal := i // ① 拷贝,retVal=10(i 的变化再也影响不到 retVal)
i++ // ② defer 执行,i=11
return retVal // ③ 返回 retVal=10
4.4 综合输出题
func f1() (r int) {
defer func() { r *= 2 }()
return 5 // r=5 → r*=2 → 返回 10
}
func f2() (r int) {
r = 5
defer func() { r++ }()
return r + 1 // r = 5+1 = 6 → r++ → 返回 7
}
func f3() int {
r := 5
defer func() { r *= 2 }() // 改局部变量,无效
return r // 返回 5
}
func main() {
fmt.Println(f1()) // 10
fmt.Println(f2()) // 7
fmt.Println(f3()) // 5
}
4.5 一个隐蔽的坑:返回值是指针 / 引用类型
即使是匿名返回值,如果返回的是指针或包含指针的结构,defer 修改指针指向的内容依然「生效」(因为拷贝的是指针,指向同一块内存):
func f() *int {
i := 10
p := &i
defer func() { *p = 20 }() // 修改指针指向的值
return p // 返回的指针不变,但指向的值已被改为 20
}
func main() {
fmt.Println(*f()) // 输出:20
}
记忆口诀:命名返回值改得动,匿名返回值改不动(除非它是引用/指针)。
5. defer 底层原理
Go 的 defer 经历了三次重大的实现演进:堆分配(Go 1.12 及以前)→ 栈分配(Go 1.13)→ 开放编码(Go 1.14)。理解底层能解释性能与行为的许多细节。
5.1 _defer 结构体
每个 defer 调用在运行时用一个 runtime._defer 结构体表示(简化版):
// runtime/runtime2.go(简化)
type _defer struct {
started bool // 该 defer 是否已开始执行
heap bool // 是否堆分配
sp uintptr // 注册时的栈指针(用于判断属于哪个栈帧)
pc uintptr // 注册时的程序计数器
fn func() // 要延迟执行的函数(Go 1.18 起简化为 func())
link *_defer // 指向下一个 _defer,构成链表
// ...
}
5.2 G 上的 defer 链表
每个 goroutine(g 结构体)上有一个 _defer 字段,指向该 goroutine 当前的 defer 链表头。新注册的 defer 用「头插法」挂到链表最前面,这天然实现了 LIFO。
g._defer ──▶ [defer C] ──▶ [defer B] ──▶ [defer A] ──▶ nil
(最后注册) (最先注册)
↑
函数返回时从头开始执行:C → B → A(后进先出)
- 注册:
runtime.deferproc(早期)/deferprocStack(栈分配),头插入链表。 - 执行:函数返回前调用
runtime.deferreturn,从链表头依次取出执行。
5.3 堆分配 → 栈分配(Go 1.13)
早期每个 _defer 都通过 newdefer 在堆上分配,涉及内存分配、GC 压力,开销较大。
Go 1.13 引入栈上分配 defer:对于不在循环中、能确定数量的 defer,_defer 结构体直接分配在当前函数的栈帧上(deferprocStack),省去堆分配。官方数据显示这让 defer 开销降低约 30%。
5.4 开放编码 defer(Go 1.14,open-coded defer)
Go 1.14 引入了革命性的 open-coded defer(开放编码 defer):在满足条件时,编译器不再走链表机制,而是把 defer 调用直接内联展开到函数返回处,几乎达到「零成本」——性能接近手写清理代码。
触发开放编码优化的条件:
| 条件 | 说明 |
|---|---|
| defer 不在循环体内 | 循环内 defer 数量运行时才确定,无法静态展开 |
| 函数内 defer 总数 ≤ 8 | 超过 8 个退回链表方式 |
defer 数 × return 数 ≤ 15 |
展开代码量的上限,防止代码膨胀 |
未禁用(如 -gcflags=-N 关优化时不启用) |
调试模式退化 |
实现要点:延迟比特(defer bit)
编译器为每个 open-coded defer 分配一个 bit 位,组成一个 8 位的 deferBits。执行到 defer 语句时置位对应 bit;函数返回时按 bit 逆序检查并执行,从而在有条件 defer(如 if 里的 defer)时也能正确判断哪些该执行。
func f(cond bool) {
defer fmt.Println("A") // bit 0
if cond {
defer fmt.Println("B") // bit 1,只有 cond 为真才置位
}
defer fmt.Println("C") // bit 2
// 返回时:检查 deferBits,逆序执行置位的 defer
}
性能对比(近似量级,具体因机器而异):
每次 defer 的大致开销演进:
Go 1.12(堆分配) ~50 ns ████████████████████
Go 1.13(栈分配) ~35 ns ██████████████
Go 1.14(开放编码) ~1-2 ns █ ← 接近直接函数调用,提升约 30 倍
结论:Go 1.14+ 里,在非循环、defer 数量少的常规场景下,defer 的性能开销已可忽略,不必再为性能而回避 defer。
6. defer 性能与循环内陷阱
6.1 版本演进小结
| Go 版本 | 实现方式 | 相对开销 |
|---|---|---|
| ≤ 1.12 | 堆分配 _defer |
高(基准 100%) |
| 1.13 | 栈分配 _defer |
约降 30% |
| 1.14+ | 开放编码(条件满足时) | 接近零(约为直接调用) |
6.2 循环内 defer 的坑
循环内的 defer 无法触发开放编码优化(回退到链表方式),且更严重的问题是:defer 只在函数返回时才执行,循环期间资源会一直累积不释放。
// ❌ 反例:循环内 defer,文件句柄要等 processAll 整个返回才全部关闭
func processAll(files []string) error {
for _, name := range files {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close() // 危险!10000 个文件 = 10000 个句柄同时打开
// ... 处理 f ...
}
return nil // 到这里才一次性关闭所有文件,可能早已 "too many open files"
}
解决方案一:抽出一个函数,让 defer 随内层函数及时返回
// ✅ 正解:每次迭代调用一个独立函数,f.Close 在每轮结束时执行
func processAll(files []string) error {
for _, name := range files {
if err := processOne(name); err != nil {
return err
}
}
return nil
}
func processOne(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close() // 每次 processOne 返回就关闭,句柄不会累积
// ... 处理 f ...
return nil
}
解决方案二:不用 defer,手动在循环体内关闭
func processAll(files []string) error {
for _, name := range files {
f, err := os.Open(name)
if err != nil {
return err
}
// ... 处理 f ...
f.Close() // 手动、及时关闭(但要注意 panic 时不会执行)
}
return nil
}
经验:只要 defer 出现在循环里,就要警惕——要么它拖累性能(回退链表),要么它导致资源延迟释放。首选「抽函数」方案。
7. panic 机制
panic 表示程序遇到了无法继续执行的严重错误。它会中断当前函数的正常流程,逐层向上「栈展开」,沿途执行每个函数已注册的 defer,直到被 recover 捕获,或到达 goroutine 顶层导致程序崩溃。
7.1 触发 panic 的方式
// 方式一:显式调用 panic
panic("something went wrong")
// 方式二:运行时错误自动触发 panic
var p *int
_ = *p // 空指针解引用 → panic: nil pointer dereference
s := []int{1, 2}
_ = s[5] // 越界 → panic: index out of range
var m map[string]int
m["k"] = 1 // 向 nil map 写入 → panic: assignment to nil map
a, b := 1, 0
_ = a / b // 除以零 → panic: integer divide by zero
7.2 栈展开(stack unwinding)与 defer
panic 发生后,程序不是立即崩溃,而是开始栈展开:从当前函数开始,逐层向调用者回退,每一层的 defer 都会照常执行。
func level3() {
defer fmt.Println("level3 defer")
panic("boom at level3") // 在这里爆炸
}
func level2() {
defer fmt.Println("level2 defer")
level3()
fmt.Println("level2 这行不会执行") // 被跳过
}
func level1() {
defer fmt.Println("level1 defer")
level2()
}
func main() {
level1()
}
// 输出:
// level3 defer ← panic 栈展开,逐层执行 defer
// level2 defer
// level1 defer
// panic: boom at level3 ...(随后程序崩溃退出,退出码 2)
栈展开图示:
调用栈 panic 传播方向
┌──────────┐
│ main │ ◀── ④ 无 recover,程序崩溃
├──────────┤
│ level1 │ ◀── ③ 执行 level1 defer
├──────────┤
│ level2 │ ◀── ② 执行 level2 defer
├──────────┤
│ level3 │ ◀── ① panic!执行 level3 defer
└──────────┘
panic 从下往上传播,沿途 defer 全部执行
7.3 panic 传播与多重 panic
如果在处理 panic 的 defer 中又发生了新的 panic,Go 会记录这条 panic 链,最终报告最后一个 panic(并显示前面的 panic 信息):
func main() {
defer func() {
panic("第二个 panic(在 defer 中)")
}()
panic("第一个 panic")
}
// 输出(简化):
// panic: 第一个 panic
// panic: 第二个 panic(在 defer 中) [recovered 链]
8. recover 机制
recover 是唯一能「拦截」panic、让程序恢复正常执行的内置函数。它有一条铁律:只有在 defer 函数中直接调用 recover 才有效。
8.1 recover 的正确用法
func safeCall() {
defer func() {
if r := recover(); r != nil {
// r 是 panic 传入的值(这里是字符串 "危险操作")
fmt.Println("已恢复,panic 值为:", r)
}
}()
panic("危险操作")
}
func main() {
safeCall()
fmt.Println("main 继续正常执行") // 会被打印!panic 已被 recover 拦下
}
// 输出:
// 已恢复,panic 值为: 危险操作
// main 继续正常执行
8.2 recover 的返回值
- 当正在 panic 且在 defer 中调用:返回 panic 传入的值(
interface{}类型),并终止栈展开,让程序回到正常流程。 - 当没有 panic 时调用:返回
nil,什么也不做。
func demo() {
r := recover() // 没有 panic 时调用
fmt.Println(r) // 输出:<nil>
}
8.3 recover 的原理
recover 之所以能生效,本质是:栈展开过程中,runtime 在执行每个 defer 前会检查——如果这个 defer 里调用了 recover,就把当前 panic 标记为「已恢复」(设置 _panic.recovered = true),停止继续向上展开,并让程序从「被 recover 的那个函数」正常返回。
panic 传播中……
↓ 执行某个 defer
↓ 该 defer 调用了 recover()
↓ runtime 标记 panic.recovered = true
↓ 停止栈展开
↓ 从当前 defer 所属的函数正常返回(可配合命名返回值设置返回值)
程序恢复正常 ✅
8.4 错误用法演示(recover 失效的情形)
错误一:直接调用 recover,没放在 defer 里
func wrong1() {
recover() // ❌ 不在 defer 中,返回 nil,毫无作用
panic("boom")
}
错误二:recover 在被 defer 调用的函数的更深一层
func doRecover() {
// ❌ recover 不是被 defer 直接调用的(中间隔了一层 doRecover 的调用栈)
if r := recover(); r != nil {
fmt.Println("这里 recover 不到")
}
}
func wrong2() {
defer doRecover() // defer 调的是 doRecover,recover 在 doRecover 内部再调
panic("boom") // ❌ recover 失效,程序仍会崩溃
}
注意:
recover必须由defer直接调用的那个函数来执行。上例中defer doRecover()里,recover()是被doRecover调用的,而不是被 defer 直接调用的匿名函数调用的,所以无效。
正确写法应把 recover 写进 defer 的匿名函数里:
func right() {
defer func() {
if r := recover(); r != nil { // ✅ defer 直接调用的函数里 recover
fmt.Println("恢复:", r)
}
}()
panic("boom")
}
错误三:recover 后想「继续原来的代码」是做不到的
func wrong3() {
defer func() { recover() }()
fmt.Println("A")
panic("boom")
fmt.Println("B") // ❌ panic 之后的代码永远不执行,recover 只能让函数「正常返回」
}
// recover 不是 try-catch 的 continue,它只能让当前函数提前正常结束
9. panic / recover 最佳实践
9.1 不要把 panic/recover 当异常处理用
Go 的错误处理哲学是用 error 显式返回,panic 只用于「不可恢复的、程序员级别的严重错误」。
// ❌ 不要这样:用 panic 做常规错误控制流
func getUser(id int) User {
if id <= 0 {
panic("invalid id") // 滥用!调用方难以处理
}
// ...
}
// ✅ 应该这样:返回 error
func getUser(id int) (User, error) {
if id <= 0 {
return User{}, errors.New("invalid id")
}
// ...
}
9.2 何时该用 panic?
- 程序初始化阶段的致命错误:如配置加载失败、必需的资源不可用,继续运行没有意义。
- 不可能发生的逻辑分支:断言程序内部不变量(如
switch的default里panic("unreachable"))。 - 库中提供
MustXxx版本:如regexp.MustCompile、template.Must,约定俗成用 panic 简化调用。
// 标准库风格:Must 版本用 panic
var re = regexp.MustCompile(`\d+`) // 编译失败直接 panic(多用于包级变量初始化)
9.3 goroutine 中的 panic 不能被其他 goroutine recover
这是极其重要的一条:recover 只能捕获同一个 goroutine 内的 panic。子 goroutine 里的 panic,主 goroutine 的 defer/recover 是拦不住的——子 goroutine panic 会直接导致整个程序崩溃。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("主 goroutine 恢复:", r) // ❌ 永远收不到子 goroutine 的 panic
}
}()
go func() {
panic("子 goroutine 爆炸") // 直接让整个进程崩溃
}()
time.Sleep(time.Second)
fmt.Println("main 结束") // 不会执行到
}
// 结果:程序崩溃,主 goroutine 的 recover 完全无效
正解:每个可能 panic 的 goroutine 自己内部 recover
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic 已恢复: %v\n%s", r, debug.Stack())
}
}()
fn()
}()
}
9.4 middleware 中统一 recover
Web 框架里的经典模式:在中间件层用 defer + recover 兜底,防止单个请求的 panic 拖垮整个服务。
// HTTP 中间件:统一恢复 panic,返回 500 而不是让服务崩溃
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r) // 该请求内的 panic 会被上面的 defer 兜住
})
}
因为每个 HTTP 请求通常在独立 goroutine 中处理,中间件的 recover 与 handler 在同一 goroutine,所以能生效——这正好呼应了 9.3 的规则。
9.5 recover 后应记录日志 / 上报
recover 不应「静默吞掉」panic。至少要记录堆栈(debug.Stack())便于排查,避免问题被隐藏。
10. 高频面试题
10.1 defer 执行顺序
Q:下面输出什么?
func main() {
defer fmt.Print("A")
defer fmt.Print("B")
defer fmt.Print("C")
}
A:CBA。多个 defer 按 LIFO(后进先出)执行,最后注册的 C 最先执行。
10.2 defer 修改返回值
Q:下面两个函数分别返回什么?
func a() (r int) {
defer func() { r++ }()
return 10
}
func b() int {
r := 10
defer func() { r++ }()
return r
}
A:a() 返回 11,b() 返回 10。return 分两步(先给返回值变量赋值,再执行 defer);a 用命名返回值 r,defer 改的就是它,故 11;b 是匿名返回值,return r 已把 10 拷贝给匿名返回值变量,defer 改的是局部 r,影响不到返回值,故 10。
10.3 defer 参数求值时机
Q:下面输出什么?
func main() {
i := 0
defer fmt.Println("defer:", i)
defer func() { fmt.Println("closure:", i) }()
i = 10
}
A:
closure: 10
defer: 0
LIFO 使闭包先执行。defer fmt.Println("defer:", i) 在注册时就把 i=0 拍照存档;闭包捕获的是 i 的引用,执行时读到最终值 10。
10.4 开放编码 defer
Q:什么是 open-coded defer?它解决了什么问题?触发条件是什么?
A: open-coded defer 是 Go 1.14 引入的优化:编译器把满足条件的 defer 调用直接内联展开到函数返回处,不再走运行时的 _defer 链表,性能接近直接函数调用(比堆分配时代快约 30 倍)。它通过 deferBits(延迟比特)标记哪些 defer 需要执行,从而支持条件 defer。触发条件:defer 不在循环中、函数内 defer 数 ≤ 8、defer 数 × return 数 ≤ 15、且未关闭编译器优化。
10.5 recover 为什么必须在 defer 里?
Q:为什么 recover 只能在 defer 函数中生效?
A: 因为 panic 发生后程序进入栈展开流程,此时正常代码已经不再执行,只有各层已注册的 defer 会被 runtime 依次调用。recover 的设计就是让 runtime 在执行 defer 时检查是否调用了 recover,若有则终止栈展开、标记 panic 已恢复。所以只有在「栈展开时仍会被执行」的 defer 中调用 recover 才能拦到 panic;在正常代码里调用 recover 时并没有正在传播的 panic,只会返回 nil。而且必须是被 defer 直接调用的那个函数来调用 recover,多包一层函数也会失效。
10.6 子 goroutine 的 panic 能被主 goroutine recover 吗?
Q:主 goroutine 里 defer recover,子 goroutine panic,能被恢复吗?
A:不能。recover 只能捕获同一个 goroutine 内的 panic。每个 goroutine 有独立的栈和 defer 链表,子 goroutine 的 panic 只会在它自己的栈上展开;若它内部没有 recover,会直接导致整个进程崩溃,主 goroutine 的 recover 无能为力。正确做法是在每个可能 panic 的 goroutine 内部自己 defer + recover。
10.7 附加:defer 在什么情况下不会执行?
A: 主要两种:① 代码根本没执行到 defer 语句(如提前 return 或前面已 panic);② 程序直接调用 os.Exit() 退出——os.Exit 不会执行任何 defer,直接终止进程。
func main() {
defer fmt.Println("这行不会打印") // os.Exit 跳过所有 defer
os.Exit(0)
}
小结
本章系统梳理了 Go 的 defer、panic、recover 三大机制:
- defer 基础:延迟到函数返回前执行,常用于关文件、解锁、recover 兜底;注册在「执行到 defer 语句」的那一刻完成。
- LIFO 顺序:多个 defer 后进先出,天然契合「后申请资源先释放」的语义。
- 参数预计算:
defer f(x)在注册时就对x求值存快照;闭包则是执行时才读引用值——「参数拍照存档,闭包现场取值」。 - 与返回值:
return分两步(赋值 → 执行 defer → 真正返回),命名返回值能被 defer 修改,匿名返回值不能(引用/指针除外)。 - 底层原理:
_defer结构体挂在 g 的链表上(头插实现 LIFO);实现经历「堆分配 → Go 1.13 栈分配 → Go 1.14 开放编码」三次演进。 - 性能:Go 1.14+ 常规场景 defer 开销可忽略;但循环内 defer 既拖性能又拖资源释放,应抽函数或手动关闭。
- panic:中断流程、逐层栈展开、沿途执行 defer,未被 recover 则崩溃退出(退出码 2)。
- recover:只能在被 defer 直接调用的函数中生效,能终止栈展开让程序恢复正常。
- 最佳实践:优先用 error,panic 留给致命/不可达错误;goroutine 内 panic 需各自 recover;Web 中间件统一兜底并记录堆栈。
理解「return 两步走」和「参数预计算 vs 闭包」这两个核心,几乎能解开所有 defer 输出题;记住「recover 只在 defer 里、只在同 goroutine 内生效」,就掌握了 panic/recover 的使用边界。
xingliuhua