Go里的panic、defer、recover到底怎么配合

Go 不用 try-catch,改用 panic、defer、recover 三件套。panic 抛异常,defer 保底执行,recover 捞回来继续跑。这套机制比传统异常处理更灵活,代码也更干净。

Go 里异常处理没有 try-catch,看起来像是少了东西,实际上用 panic、defer、recover 这三件套才是真正的大智若愚。

panic 抛异常的时候

panic 是 Go 里的炸弹,一旦丢出去,程序就开始崩塌。

通常我们不建议乱用 panic,应该用返回 error 这种温和的方式(接收方可以选择处理或忽略)。但有些场景确实无路可走,比如数组越界、nil 指针这种逻辑错误,就只能 panic。

// 主动 panic
func divide(a, b int) int {
    if b == 0 {
        panic("不能除以0啊老哥")
    }
    return a / b
}

// 被动 panic(比如 nil 指针)
var p *int
x := *p  // 直接 panic: runtime error: invalid memory address
​

当 panic 发生时,程序不是立马就死,而是开始向上逐层返回——这个时候就轮到 defer 出手了。

defer 保底执行

defer 是 Go 的"临终遗言"机制:函数执行过程中遇到 panic,或者正常返回前,所有 defer 语句都会按后进先出(LIFO) 的顺序执行,就像一个压栈的过程。

func cleanup() {
    defer func() { fmt.Println("清理第三个") }()
    defer func() { fmt.Println("清理第二个") }()
    defer func() { fmt.Println("清理第一个") }()
  
    fmt.Println("做主要工作")
}

// 输出
// 做主要工作
// 清理第一个
// 清理第二个
// 清理第三个
​

所以 defer 的常见用途就是资源回收:关闭文件、释放锁、关闭数据库连接。

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // 无论后面发生啥,这行保证会执行
  
    return ioutil.ReadAll(f)
}
​

recover 捞回来

panic 一旦发生,defer 里的代码开始执行,这时候就有机会用 recover 捞一把。

recover 只在 defer 里有效,作用是拦截 panic 并返回它的值,让程序继续活下去;如果没有 panic,recover 返回 nil。

func safeRun() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("捞回来了:%v\n", err)
        }
    }()
  
    panic("我要挂了")
    fmt.Println("这行永远不会执行")  // panic 后面的代码直接跳过
}

// 输出
// 捞回来了:我要挂了
​

注意:recover 后程序是活的,但不会回到 panic 那个位置继续执行,而是直接返回当前函数。

三件套完整配合

通常我们会这样用:先准备好兜底的 defer,里面放 recover 逻辑;执行核心代码,如果 panic 了就被 defer 捞回来。

func process(data []int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("数据处理崩溃了:%v\n", err)
            // 这里可以清理、记日志、或者继续往外抛
        }
    }()
  
    // 核心代码
    for i := 0; i < len(data)+5; i++ {  // 故意越界
        fmt.Printf("处理 %d\n", data[i])
    }
}

func main() {
    process([]int{1, 2, 3})
    fmt.Println("程序继续")
}

// 输出
// 处理 1
// 处理 2
// 处理 3
// 数据处理崩溃了:runtime error: index out of range [3] with length 3
// 程序继续
​

进阶:在 recover 里重新抛

有时候我们想捞回来但不想完全吞下这个异常,可以在 recover 里再 panic 一次,让它继续往上传播。

func middlewareRecover() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("中间件捞到 panic:%v\n", err)
            // 记个日志
            panic(err)  // 再抛一次,让上层继续处理
        }
    }()
  
    panic("原始 panic")
}

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("顶层捞到:%v\n", err)
        }
    }()
  
    middlewareRecover()
}

// 输出
// 中间层捞到 panic:原始 panic
// 顶层捞到:原始 panic
​

这样我们就可以实现一个日志记录中间件的效果:在某一层拦截 panic、记好日志,再继续向上传播,让顶层有机会做统一的错误处理。

注意事项

recover 只能捞同一个 goroutine 里的 panic。如果在另一个 goroutine 里 panic,当前 goroutine 的 recover 是捞不到的。

go func() {
    panic("我在 goroutine 里 panic 了")  // 这会导致整个程序崩溃
}()

defer func() {
    recover()  // 捞不到上面那个 panic
}()

time.Sleep(time.Second)
​

所以起 goroutine 的时候,如果有风险,最好在 goroutine 内部就做好 recover。

go func() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("goroutine 捞到:%v\n", err)
        }
    }()
  
    panic("这次能捞住")
}()

time.Sleep(time.Second)
​

小结

panic、defer、recover 的组合其实很简洁:panic 抛问题、defer 保底执行、recover 捞异常。这套逻辑比 try-catch-finally 多了一份灵活性——defer 不是只为异常存在,平时用来做资源回收才是主要用途,panic 和 recover 反而是辅助角色。

理解了这三件套的配合,Go 的异常处理就没啥神秘的了,关键是记住 recover 只在 defer 里有效、只能捞同 goroutine 的 panic 这两点,大多数坑就避开了。

更多推荐

章节目录