defer 不能捕获 panic,必须配合 recover 且需在 defer 函数体内调用;recover 仅在 panic 后、函数返回前于同一 goroutine 的 defer 中生效,否则返回 nil。

defer 不能直接捕获 panic,必须配合 recover
很多人误以为 defer 本身能“恢复” panic,其实它只是延迟执行函数;真正起作用的是 recover(),且必须在 defer 调用的函数内部调用才能生效。如果 recover() 写在普通函数里、或写在 panic 发生之后但没被 defer 包裹,它会返回 nil,不起作用。
常见错误现象:panic: runtime error: index out of range 依然崩溃,控制台没输出任何 recover 日志。
-
recover()只在 goroutine 的 defer 函数中有效,且仅对当前 goroutine 的 panic 生效 - 必须在 panic 触发后、函数返回前执行到
recover()才能捕获;一旦函数栈已展开完毕(比如 panic 后又 return),就失效 - 多个 defer 按后进先出顺序执行,
recover()应放在最靠近 panic 的那个 defer 里(或确保它在 panic 后第一个被执行)
recover 必须在 defer 函数体内调用,且不能跨 goroutine
下面这段代码看似合理,实则无效:
func badExample() {
defer recover() // ❌ 错误:recover 不是 defer 的参数,这里只是立即调用并忽略返回值
panic("boom")
}
正确写法是把 recover() 放进一个匿名函数或命名函数中,由 defer 延迟执行:
func goodExample() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("boom")
}
- 不能写成
defer recover()或defer recover—— 这会立即执行recover(),此时还没 panic,返回nil - 不能在新 goroutine 中调用
recover():go func() { recover() }()永远返回nil,因为 panic 不跨 goroutine 传播 - 如果函数有多个 panic 点,一个 defer + recover 可捕获所有(只要它们在同一个函数内、且未提前 return)
recover 后函数仍会返回,需注意返回值逻辑
recover() 并不会“撤销” panic,它只是让程序继续执行 defer 后面的代码。但原函数已经处于 panic 状态,若不显式 return,可能触发二次 panic 或返回零值。
典型问题场景:带返回值的函数中 panic 后 recover,但忘记 return,导致返回未初始化的变量:
func risky() (result string) {
defer func() {
if r := recover(); r != nil {
result = "default"
}
}()
panic("fail")
return "success" // 这行永远不会执行
}
- 推荐使用命名返回值 + defer 赋值,确保 recover 后有明确结果
- 避免在 recover 后继续执行可能出错的逻辑(比如再调一次可能 panic 的函数),否则可能掩盖真实问题
- recover 不是错误处理的常规手段,适合边界兜底(如 HTTP handler、插件加载),不适合替代 if err != nil
HTTP handler 中 recover 的典型用法和坑
Web 服务中常用 defer + recover 防止 panic 导致整个 server crash,但要注意:recover 只对当前 handler goroutine 有效,不影响其他请求。
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("panic recovered: %v", r)
}
}()
// 可能 panic 的业务逻辑
panic("unexpected error")
}
- 不要在中间件里用 recover 捕获下游 handler 的 panic 后继续调用 next() —— recover 后 handler 已退出,next() 不会执行
- recover 捕获到的
r是 interface{} 类型,可能是string、error或自定义 panic 值,建议用fmt.Sprintf("%v", r)统一转字符串打日志 - 某些 panic(如
runtime.Goexit())无法被 recover 捕获,这类属于非错误性退出,不属于异常场景
recover 是 Go 里少数几个必须和 defer 绑定使用的内置函数,它的行为高度依赖执行时机和调用位置。写错一行,比如少个 func() { ... }(),整个恢复逻辑就完全失效。实际编码时,建议把 recover 封装成可复用的 defer 函数,减少手误。











