目录

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
}

用伪代码展开 anonReturnreturn 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?

  • 程序初始化阶段的致命错误:如配置加载失败、必需的资源不可用,继续运行没有意义。
  • 不可能发生的逻辑分支:断言程序内部不变量(如 switchdefaultpanic("unreachable"))。
  • 库中提供 MustXxx 版本:如 regexp.MustCompiletemplate.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() 返回 11b() 返回 10return 分两步(先给返回值变量赋值,再执行 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 的 deferpanicrecover 三大机制:

  1. defer 基础:延迟到函数返回前执行,常用于关文件、解锁、recover 兜底;注册在「执行到 defer 语句」的那一刻完成。
  2. LIFO 顺序:多个 defer 后进先出,天然契合「后申请资源先释放」的语义。
  3. 参数预计算defer f(x) 在注册时就对 x 求值存快照;闭包则是执行时才读引用值——「参数拍照存档,闭包现场取值」。
  4. 与返回值return 分两步(赋值 → 执行 defer → 真正返回),命名返回值能被 defer 修改,匿名返回值不能(引用/指针除外)。
  5. 底层原理_defer 结构体挂在 g 的链表上(头插实现 LIFO);实现经历「堆分配 → Go 1.13 栈分配 → Go 1.14 开放编码」三次演进。
  6. 性能:Go 1.14+ 常规场景 defer 开销可忽略;但循环内 defer 既拖性能又拖资源释放,应抽函数或手动关闭。
  7. panic:中断流程、逐层栈展开、沿途执行 defer,未被 recover 则崩溃退出(退出码 2)。
  8. recover:只能在被 defer 直接调用的函数中生效,能终止栈展开让程序恢复正常。
  9. 最佳实践:优先用 error,panic 留给致命/不可达错误;goroutine 内 panic 需各自 recover;Web 中间件统一兜底并记录堆栈。

理解「return 两步走」和「参数预计算 vs 闭包」这两个核心,几乎能解开所有 defer 输出题;记住「recover 只在 defer 里、只在同 goroutine 内生效」,就掌握了 panic/recover 的使用边界。