recover只能捕获同goroutine中defer内直接调用的panic,不能捕获error;它不回滚副作用,仅防止崩溃,业务错误应返回error而非panic。

recover 不能捕获用户自定义函数“逻辑异常”,它只响应 panic;所谓“处理用户自定义函数异常”,本质是主动用 panic 触发,再用 recover 拦截——不是 catch error,而是用 panic 做控制流跳转。
recover 必须在 defer 中直接调用,且只能捕获同 goroutine 的 panic
很多人写成这样:
func handle() {
defer func() {
myRecover() // ❌ 失效:recover 不在 defer 函数体内直接调用
}()
panic("user error")
}
func myRecover() {
if r := recover(); r != nil {
fmt.Println(r)
}
}
这会导致 recover() 返回 nil,因为 Go 要求它必须是 defer 函数体内的“直接调用”。正确写法是:
- 把
recover()放进 defer 匿名函数里,且不能外包一层函数 - 即使 panic 发生在子函数里,只要没跨 goroutine,外层 defer 仍能捕获
- 多个 defer 注册时,只有最后注册(即最内层)的那个
recover()能真正拿到 panic 值,其余返回nil
用 panic 包装用户错误时,别混淆 error 和 panic 的语义
常见误区是把可预期的业务错误(比如参数校验失败、数据库查不到记录)也用 panic 抛出,然后靠 recover 捕获并转成 HTTP 400。这会破坏调用契约,让使用者无法静态判断哪些路径可能中断。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 适合
panic的场景:nil指针解引用、类型断言失败、初始化配置缺失、全局状态损坏 - ❌ 不该用
panic的场景:用户名重复、密码错误、请求参数格式不对——这些应返回error并由上层if err != nil显式处理 - 若真要统一兜底(如 Web 中间件),
panic只用于拦截运行时崩溃或框架级不可恢复错误,不替代业务错误分支
recover 后无法继续执行原函数逻辑,副作用不会自动回滚
这是最容易被忽略的一点:哪怕你成功 recover() 了,panic 发生点之后的代码也不会运行,而且之前已发生的副作用(比如往 map 写了数据、往 channel 发了消息、文件已写入部分字节)全都不会撤销。
-
recover()的作用只是“避免进程退出”,不是“时光倒流” - 如果函数里先修改了全局状态,再
panic,recover 后那个状态就留在那里了 - 想实现“失败降级”,得靠显式设计:比如用临时变量组装结果,确认无误再赋值;或用闭包封装可回滚操作
- HTTP 中间件里 recover 后,必须手动设置 status code、写响应体,否则客户端收不到任何响应
实际 Web 中间件中 recover 的最小可靠写法
生产环境常见的错误写法是只打印日志,不返回响应。下面这个片段是能跑通、有日志、有状态码、有响应体的最小闭环:
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
// 记录结构化日志(非 debug.PrintStack)
log.Printf("PANIC in %s %s: %v", r.Method, r.URL.Path, r)
// 必须显式返回错误响应
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
注意:defer 必须在 next.ServeHTTP 之前注册,否则 panic 发生在中间件之后就捕获不到;http.Error 是必须的,不能只打日志就结束函数。


















