初学 Go 的人常会问:Go 没有 try-catch,那运行时炸了怎么办?
答案就是 panic 和 recover。它们长得像别的语言的 throw/catch,但脾气完全不同——更克制、更有边界。
panic:把火警拉响
panic 一旦触发,当前 goroutine 不再往下走,而是开始「栈展开」:一层层往回退,每退一层执行该层的 defer,直到有 recover 接住,或者整个程序退出。
两种触发方式:
- 你主动拉:
panic("divide by zero") - 运行时替你拉:数组越界、空指针解引用、类型断言失败、往已关闭的 channel 发数据、并发读写 map……
参数可以是任意类型,叫 panic value。传个 error 比传字符串更好,下游 recover 时方便判断。
recover:唯一的灭火器,且只能在 defer 里用
recover 有个死规矩:必须在 defer 的闭包里直接调用。脱离 defer,它永远返回 nil,啥也接不住。
defer func() {
if r := recover(); r != nil {
fmt.Println("接住了:", r)
}
}()
为什么非得 defer?因为 panic 触发后,普通代码已经没机会执行了,只有 defer 注册的函数还会被调用。recover 就是借着 defer 这个「发言人位」抢回控制权。
标准配合姿势
把 recover 塞进 defer,外面包一层「安全函数」,捕获后转成 error 返回:
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("内部错误: %v", r)
result = 0
}
}()
if b == 0 {
panic("divide by zero")
}
return a / b, nil
}
给 goroutine 套个盔甲
最关键的踩坑点:子 goroutine 的 panic,父 goroutine 接不住。一个没防护的 goroutine panic,整个程序就挂了。所以每个 goroutine 都该自带 recover:
func runSafely(fn func()) {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine 崩了: %v\n%s", r, debug.Stack())
}
}()
fn()
}
go runSafely(func() { /* 危险操作 */ })
几个高频翻车点
defer recover()没用——返回值被丢,panic 照传。必须闭包包起来。- 跨 goroutine 接不到——recover 只对自己所在函数负责。
- 别用 panic 替代 error:error 是 Go 的常规错误处理,panic 留给「真不该发生」的情况。
- 并发读写 map 引发的 panic,应改用
sync.Map或加锁,而不是 try-recover 绕过。
一句话记忆
panic 是失火警报,recover 是灭火器,但只能在 defer 这个发言位上用,且只管自己这一层。库内部该 panic 就 panic,服务入口和 goroutine 边界该 recover 就 recover——边界划清楚,程序就稳了。