Skip to content

1.15 异常机制:panic 和 recover

Go 语言在错误处理上推崇“显式错误处理”,与许多其他语言中的 try-catch 机制不同,Go 语言将错误视为函数返回值的一部分,使用 panicrecover 机制来处理那些无法恢复的、导致程序崩溃的“异常”情况。

一、error 接口

在 Go 语言中,最常用的错误处理方式是返回一个实现了内置 error 接口的值。

1. error 接口定义

error 是一个内置接口,定义非常简单:

go
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误类型。

2. 创建和返回错误

标准库中提供了两种创建 error 对象的方法:

函数用法描述
errors.New(text string)errors.New("文件未找到")创建一个包含给定错误信息的简单错误。
fmt.Errorf(format string, a ...interface{})fmt.Errorf("无效参数: %v", arg)格式化输出错误信息,通常用于包含变量或上下文信息。

示例代码:

go
package main

import (
    "errors"
    "fmt"
)

// 定义一个表示除零错误的变量
var ErrDivByZero = errors.New("division by zero")

// div 函数返回结果和错误
func div(x, y int) (int, error) {
    if y == 0 {
        // 返回 0 和自定义错误
        return 0, ErrDivByZero
    }
    // 返回计算结果和 nil(表示没有错误)
    return x / y, nil
}

func main() {
    // 成功调用
    result1, err1 := div(10, 2)
    if err1 != nil {
        fmt.Println("错误:", err1)
    } else {
        fmt.Println("10 / 2 =", result1) // 输出: 10 / 2 = 5
    }

    // 失败调用(除零)
    result2, err2 := div(10, 0)
    if err2 != nil {
        // 可以通过比较错误变量来判断具体错误类型
        if errors.Is(err2, ErrDivByZero) {
            fmt.Println("捕获到除零错误:", err2)
        } else {
            fmt.Println("其他错误:", err2)
        }
    } else {
        fmt.Println("10 / 0 =", result2)
    }
}

3. 错误判断

处理错误时,通常使用以下方法:

  • if err != nil: 这是最常见的方式,用来检查函数调用是否返回了错误。
  • errors.Is(err, target): 用于判断错误链中是否包含特定的错误值(例如上面定义的 ErrDivByZero),这是进行错误类型判断的标准方式。
  • errors.As(err, &target): 用于判断错误链中是否可以解包(Unwrap)出特定类型的错误,并将其赋值给 target 变量。

二、panicrecover

panicrecover 是 Go 语言处理不可恢复错误的机制,类似于其他语言中的异常抛出和捕获。

惯例: 只有当程序遇到无法继续运行的严重错误(如数组越界、空指针引用或关键服务失败)时,才应该使用 panic。对于预期的、可恢复的错误,应使用 error 接口。

1. panic:抛出异常,终止程序

panic 是一个内置函数,用于中断正常的控制流程。

  • 当函数 F 中执行 panic 时,F 之后的代码将不再执行
  • 程序会沿着调用栈向外层函数返回,执行当前 Goroutine 中所有已注册的 defer 函数。
  • 如果调用栈上的所有 defer 函数都执行完毕,但 panic 仍未被捕获,程序将打印堆栈信息并崩溃退出

示例:

go
func main() {
    defer fmt.Println("这是 main 函数的 defer") // 会在 panic 发生后执行
    fmt.Println("开始执行")
    
    // 手动触发 panic
    panic("发生了一个严重错误!") 
    
    fmt.Println("这行代码不会被执行")
}
// 输出: 
// 开始执行
// 这是 main 函数的 defer
// panic: 发生了一个严重错误!
// ... (堆栈信息)

2. defer:延迟调用

defer 语句用于延迟一个函数的执行,直到包含它的函数(或方法)即将返回时才执行。在异常处理中,defer 是与 recover 搭配使用的关键

  • defer 语句会在其所在函数返回前执行,无论函数是正常返回还是因为 panic 而退出。
  • 多个 defer 语句的执行顺序是栈式的,即后进先出 (LIFO)

3. recover:捕获异常,恢复程序

recover 是一个内置函数,必须defer 函数中调用,才能捕获到 panic 抛出的错误,并阻止程序崩溃。

  • 有效调用:defer 函数内直接调用 recover()
  • 作用: 如果一个 panic 正在进行,recover 会捕获到 panic 的参数(即 panic() 函数的参数),并终止 panic 过程,让程序恢复正常执行流程。
  • 恢复位置: 捕获 panic 后,程序不会回到 panic 发生的点,而是继续执行 defer 之后的代码。

示例代码:

go
package main

import "fmt"

func safeDivide(a, b int) {
    // 必须在 defer 匿名函数中调用 recover
    defer func() {
        if r := recover(); r != nil {
            // r 就是 panic() 传递的参数
            fmt.Printf("程序恢复!捕获到 panic 错误: %v\n", r)
        }
    }()

    fmt.Println("尝试进行除法计算...")
    
    // 故意制造一个会导致 panic 的操作
    z := a / b // 如果 b=0,会触发 runtime panic: integer divide by zero
    
    fmt.Printf("%d / %d = %d\n", a, b, z)
}

func main() {
    safeDivide(10, 2) // 正常执行
    fmt.Println("--------------------")
    safeDivide(10, 0) // 发生 panic,但被 recover 捕获并恢复
    
    // 由于 panic 被捕获,程序可以继续执行后续代码
    fmt.Println("--------------------")
    fmt.Println("main 函数继续执行,程序没有崩溃。")
}

输出结果:

尝试进行除法计算...
10 / 2 = 5
--------------------
尝试进行除法计算...
程序恢复!捕获到 panic 错误: runtime error: integer divide by zero
--------------------
main 函数继续执行,程序没有崩溃。

三、 panic 无法跨越 Goroutine 边界

panicrecover 机制是针对单个 Goroutine 的。在一个 Goroutine 中触发的 panic,只能被该 Goroutine 调用栈上的 defer 函数捕获。

核心结论:

  • 主协程的 recover 无法捕获子协程的 panic
  • 子协程发生 panic 且未被捕获时,它会导致整个程序崩溃,而不仅仅是该子协程

子协程中的 panic

go
package main

import (
    "fmt"
    "time"
)

func main() {
    // ❌ 错误做法:这个 recover 无法捕获子协程的 panic
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("❌ 主协程捕获了错误:", r)
        }
    }()

    fmt.Println("主协程开始运行...")

    go func() {
        // 子协程中没有 recover
        fmt.Println("子协程即将 panic")
        panic("来自子协程的致命错误") // ⚠️ 这个 panic 会使整个程序崩溃!
    }()
    
    // 确保子协程有时间运行
    time.Sleep(1 * time.Second) 
    fmt.Println("主协程结束。") // 如果程序崩溃,这句不会被输出
}

在子协程内部捕获

为了保护程序不被子协程的 panic 影响,正确做法:必须在子协程的入口函数中设置 defer + recover

go
package main

import (
    "fmt"
    "time"
)

func worker() {
    // ✅ 正确做法:在子协程内部设置 recover
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("✅ 子协程恢复!错误: %v\n", r)
        }
    }()

    fmt.Println("子协程开始工作...")
    // ... 可能触发 panic 的代码 ...
    panic("来自子协程的致命错误") 
    // ... panic 后续代码不会执行 ...
}

func main() {
    go worker()
    
    // 即使 worker 发生了 panic,程序也不会崩溃
    time.Sleep(1 * time.Second) 
    fmt.Println("主协程继续运行,程序安全。")
}

四、总结与建议

机制作用适用场景Go 语言惯例
error显式返回,表示函数执行结果中的预期错误。文件未找到、网络连接失败、输入参数无效等可预测和可恢复的错误。推荐:大部分情况下使用 error 接口进行错误处理。
panic/recover抛出/捕获异常,处理程序中不可恢复的运行时错误。数组越界、空指针引用、关键服务初始化失败等不可恢复的错误。仅用于处理真正的程序异常/崩溃,或在底层库中将 panic 转换为 error