导航菜单

Defer、Panic 与 Recover

Defer、Panic 与 Recover

Go 语言提供了 deferpanicrecover 三个关键字来处理控制流异常。defer 用于延迟函数执行(通常用于资源清理),panic 用于触发运行时异常,recover 用于从 panic 中恢复。三者协同工作,构成了 Go 的异常处理机制。

defer 的执行顺序

defer(延迟调用)

defer 语句将一个函数调用推迟到当前函数返回之前执行。多个 defer 语句按照**后进先出(LIFO)**的顺序执行——类似于栈的操作,最后一个 defer 最先执行。

基本用法

func main() {
    defer fmt.Println("第 1 个 defer")
    defer fmt.Println("第 2 个 defer")
    defer fmt.Println("第 3 个 defer")

    fmt.Println("正常执行")
}
// 输出:
// 正常执行
// 第 3 个 defer
// 第 2 个 defer
// 第 1 个 defer

参数求值时机

func main() {
    i := 0
    defer fmt.Println("defer 中 i 的值:", i)  // i 在此处求值为 0

    i = 100
    fmt.Println("修改后 i 的值:", i)
}
// 输出:
// 修改后 i 的值: 100
// defer 中 i 的值: 0

defer 与闭包的注意事项

如果 defer 语句中使用闭包(匿名函数),则闭包中引用的变量在闭包执行时才被读取:

func main() {
    i := 0
    defer func() {
        fmt.Println("闭包中 i 的值:", i)  // 闭包在函数返回时执行,此时 i = 100
    }()

    i = 100
    fmt.Println("修改后 i 的值:", i)
}
// 输出:
// 修改后 i 的值: 100
// 闭包中 i 的值: 100

defer 与循环的陷阱

// ❌ 错误做法:所有 defer 在函数结束时才执行,100 个文件同时打开
func processFiles(filenames []string) error {
    for _, f := range filenames {
        file, err := os.Open(f)
        if err != nil {
            return err
        }
        defer file.Close()  // 累积到 processFiles 返回时才关闭
        // 处理 file...
    }
    return nil
    // 所有文件在这里才被关闭!
}

// ✅ 正确做法:使用包装函数让 defer 在每次迭代结束时执行
func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()  // processFile 返回时立即关闭

    // 处理文件...
    return nil
}

func processFiles(filenames []string) error {
    for _, f := range filenames {
        if err := processFile(f); err != nil {
            return err
        }
    }
    return nil
}

defer 的常见用法

1. 资源释放

这是 defer 最常见的用途,确保文件、连接等资源被正确释放:

func readConfig(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // 确保 file 被关闭

    return io.ReadAll(f)
}

2. 释放互斥锁

var mu sync.Mutex
var data map[string]int

func safeSet(key string, value int) {
    mu.Lock()
    defer mu.Unlock()  // 确保 mutex 被释放

    data[key] = value
}

func safeGet(key string) (int, bool) {
    mu.Lock()
    defer mu.Unlock()

    val, ok := data[key]
    return val, ok
}

3. 计时与性能追踪

func trace(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("%s 执行耗时: %v\n", name, time.Since(start))
    }
}

func processOrder(orderID string) {
    defer trace("processOrder")()  // 函数返回时打印耗时

    // 模拟耗时操作
    time.Sleep(200 * time.Millisecond)
    fmt.Printf("处理订单 %s 完成\n", orderID)
}

func main() {
    processOrder("ORD-001")
    // 输出:
    // 处理订单 ORD-001 完成
    // processOrder 执行耗时: 200.5ms
}

4. 修改命名返回值

defer 中的闭包可以修改函数的命名返回值:

func divide(a, b float64) (result float64, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生 panic: %v", r)
        }
    }()

    if b == 0 {
        panic("除数不能为零")
    }
    result = a / b
    return
}

func main() {
    result, err := divide(10, 0)
    fmt.Printf("result=%.2f, err=%v\n", result, err)
    // 输出: result=0.00, err=发生 panic: 除数不能为零
}

panic 的触发与传播

panic(恐慌)

panic 是 Go 的内置函数,用于触发运行时异常。当 panic 被调用时,程序会立即停止当前函数的正常执行流程,开始执行所有已注册的 defer 函数,然后将 panic 向上传播到调用者。如果没有被 recover 捕获,程序最终会崩溃退出。

触发 panic

// 1. 显式调用 panic
panic("发生了严重错误!")

// 2. 运行时自动触发(常见情况)
var p *int
*p = 42  // panic: runtime error: invalid memory address or nil pointer dereference

// 3. 数组越界
arr := [3]int{1, 2, 3}
fmt.Println(arr[10])  // panic: runtime error: index out of range [10] with length 3

// 4. 类型断言失败
var i any = "hello"
n := i.(int)  // panic: interface conversion: interface {} is string, not int

// 5. 并发读写 map
// m := make(map[int]int)
// go func() { m[1] = 1 }()
// m[2] = 2  // fatal error: concurrent map writes

panic 的传播过程

函数 C 调用 panic()

函数 C 的 defer 函数按 LIFO 顺序执行

panic 传播到函数 B

函数 B 的 defer 函数按 LIFO 顺序执行

panic 传播到函数 A

... 继续向上传播 ...

如果到达 main() 仍未被 recover,程序崩溃退出
func C() {
    fmt.Println("C 开始")
    defer fmt.Println("defer C")
    panic("C 中发生了 panic!")
    fmt.Println("C 结束")  // 不会执行
}

func B() {
    fmt.Println("B 开始")
    defer fmt.Println("defer B")
    C()
    fmt.Println("B 结束")  // 不会执行
}

func A() {
    fmt.Println("A 开始")
    defer fmt.Println("defer A")
    B()
    fmt.Println("A 结束")  // 不会执行
}

func main() {
    A()
}
// 输出:
// A 开始
// B 开始
// C 开始
// defer C
// defer B
// defer A
// panic: C 中发生了 panic!
//
// goroutine 1 [running]:
// main.C()
//     ...
// main.B()
//     ...
// main.A()
//     ...
// main.main()
//     ...

recover 捕获 panic

recover(恢复)

recover 是 Go 的内置函数,用于从 panic 中恢复。它只能在 defer 函数中直接调用才能生效。调用 recover() 会停止 panic 的传播,并返回传递给 panic 的值。

基本用法

func safeDivide(a, b float64) (result float64) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获到 panic:", r)
            result = 0
        }
    }()

    if b == 0 {
        panic("除数不能为零")
    }
    return a / b
}

func main() {
    fmt.Println(safeDivide(10, 0))  // 捕获到 panic: 除数不能为零
    fmt.Println(safeDivide(10, 3))  // 3.333...
    fmt.Println("程序继续运行")
}

recover 的关键规则

// ❌ 无效:recover 不在 defer 中
func invalid1() {
    recover()  // 无效,直接调用不会捕获任何东西
    panic("test")
}

// ❌ 无效:recover 在嵌套的函数中(间接调用)
func invalid2() {
    defer func() {
        doRecover()  // 无效!recover 必须在 defer 的函数体中直接调用
    }()
    panic("test")
}

func doRecover() {
    recover()  // 这里调用 recover 无效
}

// ✅ 有效:recover 在 defer 函数中直接调用
func valid() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("恢复:", r)
        }
    }()
    panic("test")
}

实际应用:HTTP 服务器的 panic 恢复

func safeHandler(h http.HandlerFunc) http.HandlerFunc {
    return 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, "内部服务器错误", http.StatusInternalServerError)
            }
        }()
        h(w, r)
    }
}

func main() {
    // 所有路由都包装上 panic 恢复
    http.HandleFunc("/api", safeHandler(apiHandler))
    http.ListenAndServe(":8080", nil)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    // 即使这里 panic,服务器也不会崩溃
    panic("模拟 panic")
}

panic vs error

panic vs error 的使用原则

Go 的设计哲学是:常规错误用 error,严重错误才用 panicerror 是正常的、预期的控制流,调用者应该处理它。panic 是不正常的、意外的运行时错误,通常意味着程序无法继续运行。

何时使用 error

// ✅ 使用 error 处理可预期的错误
func readFile(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", fmt.Errorf("读取文件 %s: %w", path, err)
    }
    return string(data), nil
}

何时使用 panic

// ✅ panic 的合理使用场景

// 1. 程序启动时的致命错误
func initDB() *sql.DB {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        // 程序启动失败,无法继续运行 → panic
        log.Fatalf("无法连接数据库: %v", err)
    }
    if err := db.Ping(); err != nil {
        panic(fmt.Sprintf("数据库连接测试失败: %v", err))
    }
    return db
}

// 2. 不可恢复的逻辑错误
func setAge(age int) {
    if age < 0 || age > 150 {
        // 这是编程错误,不应该发生 → panic
        panic(fmt.Sprintf("invalid age: %d", age))
    }
    // ...
}

// 3. 类型断言(确信类型正确时使用)
func process(val any) {
    // 如果你确信 val 是 string 类型
    s := val.(string)  // 如果不是 string 则 panic
    fmt.Println(s)
}

// 4. 使用 "comma, ok" 模式更安全
func processSafe(val any) {
    s, ok := val.(string)  // 安全的类型断言
    if !ok {
        fmt.Println("值不是 string 类型")
        return
    }
    fmt.Println(s)
}

对比总结

特性errorpanic
性质正常的、预期的不正常的、意外的
处理方式调用者处理除非 recover 否则崩溃
恢复性可恢复不可恢复(除非 recover)
使用频率非常频繁非常少
适用场景I/O、网络、用户输入编程错误、启动失败
Go 风格✅ 推荐⚠️ 谨慎使用

练习题

练习 1:defer 执行顺序与闭包

以下代码的输出是什么?请逐步分析。

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("defer 1:", x)
    }()

    defer func(n int) {
        fmt.Println("defer 2:", n)
    }(x)

    x := 10
    x++
    fmt.Println("x =", x)
}
参考答案

输出

x = 11
defer 2: 10
defer 1: 11

分析

  1. defer 1 是一个闭包,捕获了变量 x 的引用。执行时 x 的值为 11(x++ 之后)
  2. defer 2 是一个带参数的函数,参数 ndefer 声明时求值。此时 x = 10x++ 还未执行),所以 n = 10
  3. 正常代码执行:x = 10,然后 x++x 变为 11,打印 "x = 11"
  4. 函数返回时,按 LIFO 顺序执行 defer:
    • defer 2 先执行:打印 "defer 2: 10"(参数在声明时求值为 10)
    • defer 1 后执行:打印 "defer 1: 11"(闭包读取变量 x 的最新值 11)

关键知识点

  • defer func() { ... }() 中的闭包引用变量在执行时求值
  • defer func(n int) { ... }(x) 中的参数 xdefer 声明时求值

练习 2:panic 传播与 recover

以下代码的输出是什么?如果将 recover() 的位置从 B() 移到 A() 的 defer 中,输出会怎样?

package main

import "fmt"

func C() {
    fmt.Println("C: start")
    defer fmt.Println("C: defer")
    panic("panic in C")
    fmt.Println("C: end")
}

func B() {
    fmt.Println("B: start")
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("B: recovered:", r)
        }
    }()
    defer fmt.Println("B: defer")
    C()
    fmt.Println("B: end")
}

func A() {
    fmt.Println("A: start")
    defer fmt.Println("A: defer")
    B()
    fmt.Println("A: end")
}

func main() {
    A()
    fmt.Println("main: 继续执行")
}
参考答案

当前代码输出

A: start
B: start
C: start
C: defer
B: defer
B: recovered: panic in C
A: end
A: defer
main: 继续执行

分析

  1. C()panic("panic in C") 触发后,C 的 defer("C: defer")先执行
  2. panic 传播到 B()B 的 defers 按逆序执行:
    • 先执行 recover 的 defer(因为它声明在 "B: defer" 之后,但 recover 必须在 defer 中),panic 被捕获
    • 再执行 "B: defer"
  3. panic 在 B 中被 recover,不再向上传播
  4. B() 正常返回,A() 继续执行 "A: end""A: defer"
  5. main() 继续执行 "main: 继续执行"

如果将 recover 移到 A() 中

func B() {
    fmt.Println("B: start")
    defer fmt.Println("B: defer")  // 没有 recover
    C()
    fmt.Println("B: end")
}

func A() {
    fmt.Println("A: start")
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("A: recovered:", r)
        }
    }()
    defer fmt.Println("A: defer")
    B()
    fmt.Println("A: end")
}

输出

A: start
B: start
C: start
C: defer
B: defer
A: defer
A: recovered: panic in C
main: 继续执行

区别B() 的 defers 执行完后 panic 继续传播到 A(),在 A() 中被 recover。注意 BA"end" 都不会执行,因为 panic 跳过了它们后面的正常代码。

练习 3:实现安全的类型转换函数

编写一个函数 safeAssert[T any](val any) (T, bool),使用 recover 从类型断言失败的 panic 中恢复。

参考答案

代码

package main

import "fmt"

// SafeAssert 安全地执行类型断言,失败时返回零值和 false
func SafeAssert[T any](val any) (result T, ok bool) {
    defer func() {
        if r := recover(); r != nil {
            // 从 panic 中恢复,result 保持零值,ok 保持 false
            ok = false
        }
    }()

    result = val.(T)  // 如果类型不匹配会 panic
    ok = true
    return
}

func main() {
    // 成功的断言
    s, ok := SafeAssert[string]("hello")
    fmt.Printf("string: %q, ok=%v\n", s, ok)
    // 输出: string: "hello", ok=true

    // 失败的断言
    n, ok := SafeAssert[int]("hello")
    fmt.Printf("int: %d, ok=%v\n", n, ok)
    // 输出: int: 0, ok=false

    // nil 值的断言
    var p *int
    v, ok := SafeAssert[*int](p)
    fmt.Printf("nil *int: %v, ok=%v\n", v, ok)
    // 输出: nil *int: <nil>, ok=true(断言成功,值是 nil)

    // 对 nil 进行非匹配的断言
    f, ok := SafeAssert[float64](p)
    fmt.Printf("float64 from nil *int: %f, ok=%v\n", f, ok)
    // 输出: float64 from nil *int: 0.000000, ok=false
}

关键点

  1. 使用泛型 T any 让函数适用于任何类型
  2. defer + recover 在类型断言 panic 时优雅恢复
  3. 使用命名返回值 resultok,在 recover 中可以修改 ok 的值
  4. 注意:虽然这个函数展示了 recover 的用法,但在实际代码中直接使用 val.(T) 的 “comma, ok” 模式(v, ok := val.(T))更简单、性能更好

搜索