recover必须在defer中调用才有效,普通调用无效;需在每个goroutine内单独defer recover;应通过带缓冲channel统一收集panic错误;recover无法捕获Goexit、系统信号或Cgo崩溃;recover后禁止继续执行原逻辑,仅可记录错误和清理资源。

recover 必须在 defer 中调用,否则无效
Go 的 recover 只有在 panic 正在发生、且当前 goroutine 尚未退出时才有效。如果写成普通函数调用(比如放在逻辑中间),它什么也抓不到。必须配合 defer 才能拦截 panic —— 这是绝大多数新手踩的第一个坑。
常见错误写法:
func handleRequest() {
recover() // ❌ 永远返回 nil
panic("something went wrong")
}
正确姿势是:
- 每个可能 panic 的 goroutine 内部,都得有自己的
defer func() { recover() }() - 不要指望外层函数替内层 goroutine 拦截 panic —— goroutine 是独立的执行单元
- 如果用
go func() { ... }()启动,recover必须在该匿名函数内部 defer
用 channel 统一收集 panic 错误,避免日志丢失
高并发下,成百上千个 goroutine 同时 panic,如果每个都直接打日志或写文件,容易触发 I/O 竞争甚至阻塞。更稳妥的做法是把错误信息发到一个带缓冲的 channel,由单独的“错误处理 goroutine”消费。
关键点:
立即学习“go语言免费学习笔记(深入)”;
- channel 缓冲区大小要合理,比如
make(chan error, 1000),防止生产者因满而阻塞 - 发送前检查 channel 是否已关闭(尤其在服务 shutdown 阶段)
- 接收端要用
for err := range ch,而不是单次,否则只处理第一个错误
示例结构:
var panicCh = make(chan error, 1000)func worker(id int) { defer func() { if r := recover(); r != nil { panicCh <- fmt.Errorf("worker %d panicked: %v", id, r) } }() // ... 实际业务逻辑,可能 panic }
// 单独启动一个 goroutine 处理 panic 日志 go func() { for err := range panicCh { log.Printf("PANIC captured: %v", err) } }()
recover 无法捕获 runtime.Goexit 或 syscall 退出
recover 只对 panic 有效。以下情况它完全无能为力:
-
runtime.Goexit():主动终止当前 goroutine,不触发 panic -
操作系统信号如
SIGKILL、SIGQUIT - C 代码中调用
exit(1)或发生段错误 - 被
context.DeadlineExceeded中断的 goroutine(本身不 panic)
所以不能把 recover 当成“万能兜底”。真正健壮的服务需要:
- 用
context控制超时和取消 - 对 Cgo 调用加额外保护(比如
signal.Notify捕获 SIGSEGV) - 进程级监控(如 systemd、supervisord)来拉起崩溃后的主 goroutine
不要在 recover 后继续执行原逻辑
有人会这么写:
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r)
// 接着往下跑?❌
doNextStep() // 危险!栈已损坏,变量状态未知
}
}()
这是严重误区。panic 可能发生在任意位置,栈已部分展开,局部变量、锁、channel 状态都不可信。recover 后唯一安全的操作是:
- 记录错误
- 清理资源(如 close channel、unlock mutex)
- 返回或退出当前 goroutine
继续执行后续业务代码,大概率引发二次 panic 或数据错乱。高并发场景下这种“带伤运行”的 goroutine 很难定位,比直接崩溃更麻烦。










