很多 Go 初学者第一次遇到 panic 都会很慌:程序直接崩了,错误信息一大堆。于是就会听到一句话:recover 可以救回来

recover 并不是“哪里都能用”的万能药,它有非常明确的触发条件和边界。本文用最小示例把 panic / defer / recover 的关系讲清楚,并列出常见坑和工程里正确的兜底方式。

1. panic / defer / recover 到底是什么关系?

  • panic

    • 让当前 goroutine 进入异常流程:从当前函数开始向上“展开调用栈”(stack unwinding)
    • 在展开过程中,会执行每一层函数里注册的 defer
  • defer

    • 延迟执行:函数返回前执行
    • 发生 panic 时也会执行(这是 recover 能工作的原因)
  • recover

    • 作用:在 panic 发生后,把 panic 的值取出来,并阻止程序继续崩溃
    • 只能在 defer 调用的函数里生效,否则返回 nil

一句话总结:
panic 触发异常 -> defer 仍会执行 -> defer 里调用 recover 才能捕获并兜底。

2. 最小可运行示例:recover 正确写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()

fmt.Println("before panic")
panic("something bad happened")
fmt.Println("after panic (不会执行)")
}

运行输出类似:

  • before panic
  • 捕获到 panic: something bad happened

注意:panic 之后的那行 after panic 不会执行,因为控制流已经进入 panic 展开流程了。

3. 常见误区 1:不在 defer 里调用 recover —— 永远抓不到

很多人会写成这样:

1
2
recover() // 没用
panic("x")

或者这样:

1
2
3
4
5
func f() {
r := recover() // 没用
fmt.Println(r)
panic("x")
}

原因:recover 必须在 panic 展开过程中被调用,而展开过程只会执行 defer,所以必须写在 defer func(){...}() 里。

4. 常见误区 2:recover 只能抓“同一个 goroutine”的 panic

下面这种情况非常常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("main recover:", r)
}
}()

go func() {
panic("panic in goroutine")
}()

time.Sleep(time.Second)
fmt.Println("main exit")
}

mainrecover 抓不到子 goroutine 的 panic。子 goroutine panic 会直接把整个进程打挂(如果没人兜底)。

正确方式:在每个 goroutine 的入口处兜底

1
2
3
4
5
6
7
8
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("goroutine recover:", r)
}
}()
panic("panic in goroutine")
}()

5. 常见误区 3:recover 后不是“从 panic 那行继续往下跑”

recover 只是把 panic 截住,让程序不至于崩溃。

但它并不会让函数回到 panic() 那一行继续执行;panic 所在函数会走“返回路径”,相当于提前结束了当前执行流。

所以 recover 更像:

  • 兜底 + 收尾 + 记录 + 返回默认值/错误

而不是:

  • 异常发生了,但当作没发生一样继续跑

6. recover 到底该怎么用才合理?

6.1 建议只在“边界层”兜底(框架/入口)

比如:

  • HTTP handler 的最外层
  • 消费队列/定时任务 worker 的最外层
  • goroutine 的入口函数

原因:

  • panic 往往代表程序员错误或不变式被破坏
  • 在业务逻辑里用 recover 把 panic 吞掉,会隐藏问题,导致“悄悄错了但没报警”

6.2 兜底时应该做什么?

至少做三件事:

  • 记录 panic 的值
  • 记录堆栈(非常重要)
  • 让调用方得到合理响应(返回错误/默认值/500)

下面是一个通用的 recover 包装函数(示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"runtime/debug"
)

func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %v\nstack:\n%s\n", r, debug.Stack())
}
}()
fn()
}()
}

func main() {
safeGo(func() {
panic("boom")
})
select {}
}

7. panic 和 error 怎么选?(给初学者的简单规则)

  • error:可预期的失败(参数不合法、网络超时、文件不存在、权限不足)
  • panic:不可恢复的程序错误/不变式破坏(数组越界、空指针、并发写 map、逻辑必然不该发生)
  • recover:只在边界层兜底,避免单个请求/任务把整个进程打挂,同时保留日志和堆栈,便于修 bug

8. 小结

  • recover 必须在 defer 里才有效
  • recover 只能捕获同一 goroutine 的 panic
  • recover 的正确价值:兜底、防崩溃、打日志、保留堆栈、让系统可用
  • 日常业务错误处理:优先 error,不要靠 panic/recover 当“异常机制”