recover 必须在 defer 中调用才有效,否则立即返回 nil;它仅对当前 goroutine 生效,且 panic 后原函数后续逻辑不再执行。

recover 必须在 defer 中调用才有效
Go 没有传统 try/catch,recover 不是全局异常处理器,它只在当前 goroutine 的 panic 正在被传播、且尚未退出函数时起作用。如果没用 defer 包裹,recover 会立即返回 nil,什么也抓不到。
旧项目里常见错误是把 recover 写在普通逻辑分支里,比如:
func handleRequest() {
if err := doSomething(); err != nil {
// ❌ 这里 recover 永远不生效
if r := recover(); r != nil {
log.Printf("caught: %v", r)
}
return
}
}
正确写法必须绑定到 defer,且该 defer 要在 panic 可能发生的位置之前注册:
func handleRequest() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v", r)
// 可选:转为 error 返回,或触发降级逻辑
}
}()
doSomething() // 这里 panic 才会被捕获
}
-
defer要在可能 panic 的代码前执行,否则注册太晚 - 匿名函数里不要提前
return,否则defer不会运行 - 多个
defer按后进先出顺序执行,recover应该放在最外层的defer里
recover 只对当前 goroutine 生效
旧项目若用了 go 关键字启动子 goroutine,并在其中 panic,主 goroutine 的 recover 完全无感知——这是重构时最容易漏掉的点。
立即学习“go语言免费学习笔记(深入)”;
例如 HTTP handler 里这样写:
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "server error", http.StatusInternalServerError)
}
}()
go func() {
// ❌ 这里的 panic 不会被上面的 recover 捕获
riskyOperation()
}()
}
解决方法只有两个:
- 子 goroutine 内部自己加
defer + recover,并做好日志或通知(如发 channel、调监控) - 改用同步调用,或用
errgroup.Group统一等待+错误收集(适合需要结果聚合的场景)
别指望一个顶层 recover 管所有 goroutine——Go 的设计就是如此,强行“兜底”反而掩盖并发问题。
recover 后不能继续执行原函数逻辑
很多人误以为 recover 是“catch 并 continue”,其实不是:recover 只是让 panic 停止传播,但函数栈已经展开完毕,控制权回到 defer 所在函数的末尾,之后的代码照常执行,而原 panic 发生点之后的语句永远不会运行。
看这个典型陷阱:
func process() error {
defer func() {
if r := recover(); r != nil {
log.Println("recovered")
}
}()
data := load() // 假设这里 panic
result := transform(data) // ❌ 这行不会执行
return save(result) // ❌ 这行也不会执行
}
所以重构时要注意:不能靠 recover 来“修复”流程,而应该把它当作一种兜底出口,用于清理、记录、返回默认值或错误。常见做法包括:
- 在
defer匿名函数中设置一个闭包变量(如var result error),panic 后赋值并返回 - 直接返回预设错误,如
return errors.New("internal panic")(需函数签名支持) - 避免在关键路径上依赖 panic/recover 做业务逻辑分支——应优先用显式 error 判断
HTTP handler 中 recover 的实际封装模式
旧项目往往每个 handler 都手写一遍 defer/recover,重复又易错。推荐提取成中间件风格的包装函数:
func withRecovery(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("[PANIC] %s %s: %v", r.Method, r.URL.Path, r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next(w, r)
}
}
// 使用
http.HandleFunc("/api/data", withRecovery(handleData))
注意点:
- 别在
withRecovery里尝试恢复响应体(如重写w)——一旦WriteHeader或Write已调用,再写就 panic - 日志中的
r是指 recover 的值,不是*http.Request,别混淆变量名 - 如果 handler 里用了
http.TimeoutHandler或其他 wrapper,确保withRecovery是最外层,否则可能被跳过
真正难的不是写 recover,而是判断哪些 panic 是可预期的(比如空指针解引用)、哪些暴露了深层 bug(比如 channel 已关闭还 send)。重构时重点不是加 recover,而是减少 panic 源头——比如用 if v != nil 替代直接解引用,用 select { case 避免阻塞。


















