Go 里的 recover:如何优雅地兜底 panic(写给初学者)
很多 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 | package main |
运行输出类似:
before panic捕获到 panic: something bad happened
注意:panic 之后的那行 after panic 不会执行,因为控制流已经进入 panic 展开流程了。
3. 常见误区 1:不在 defer 里调用 recover —— 永远抓不到
很多人会写成这样:
1 | recover() // 没用 |
或者这样:
1 | func f() { |
原因:recover 必须在 panic 展开过程中被调用,而展开过程只会执行 defer,所以必须写在 defer func(){...}() 里。
4. 常见误区 2:recover 只能抓“同一个 goroutine”的 panic
下面这种情况非常常见:
1 | package main |
main 的 recover 抓不到子 goroutine 的 panic。子 goroutine panic 会直接把整个进程打挂(如果没人兜底)。
正确方式:在每个 goroutine 的入口处兜底:
1 | go func() { |
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 | package main |
7. panic 和 error 怎么选?(给初学者的简单规则)
- 用
error:可预期的失败(参数不合法、网络超时、文件不存在、权限不足) - 用
panic:不可恢复的程序错误/不变式破坏(数组越界、空指针、并发写 map、逻辑必然不该发生) - 用
recover:只在边界层兜底,避免单个请求/任务把整个进程打挂,同时保留日志和堆栈,便于修 bug
8. 小结
recover必须在defer里才有效recover只能捕获同一 goroutine 的 panic- recover 的正确价值:兜底、防崩溃、打日志、保留堆栈、让系统可用
- 日常业务错误处理:优先
error,不要靠panic/recover当“异常机制”
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Can you fell my world!
评论