recover只能在defer函数中调用,且该defer必须在panic前注册;它仅对当前goroutine当前调用栈的panic有效,无法跨goroutine捕获,也不能回滚状态或保证数据一致性,核心价值在于提升崩溃的可追溯性。

recover 只能在 defer 函数里调用
这是最常被忽略的前提:直接在普通函数体里写 recover() 永远返回 nil,根本捕不到 panic。它必须出现在 defer 注册的函数中,且该 defer 必须在 panic 发生前已注册(即 panic 前执行过 defer 语句)。
常见错误是把 recover() 放在普通逻辑块里,比如:
func badExample() {
if r := recover(); r != nil { // ❌ 永远不生效
log.Println(r)
}
panic("boom")
}
正确写法是:
func goodExample() {
defer func() {
if r := recover(); r != nil { // ✅ 在 defer 内
log.Println("caught:", r)
}
}()
panic("boom")
}
-
recover()不是全局监听器,它只对“当前 goroutine 当前调用栈上正在发生的 panic”有效 - 如果 panic 已经传播出当前函数(比如在子函数里 panic,但外层没 defer),
recover()就失效了 - 多个嵌套的 defer 都会执行,但只有第一个成功调用
recover()的能终止 panic 传播
recover 无法跨 goroutine 捕获 panic
Go 的 panic 是 goroutine 局部的——一个 goroutine 的 panic 不会自动传播到另一个,也不会被另一个 goroutine 的 recover() 捕获。这意味着你不能靠主 goroutine 的 defer 来兜住子 goroutine 的 panic。
典型反例:
func main() {
defer func() {
if r := recover(); r != nil {
log.Println("won't catch anything here")
}
}()
go func() {
panic("from goroutine") // ❌ 主 goroutine 的 recover 看不见
}()
time.Sleep(time.Millisecond)
}
要捕获子 goroutine 的 panic,必须在子 goroutine 内部自己加 defer + recover():
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine recovered: %v", r)
}
}()
panic("from goroutine")
}()
- HTTP handler、定时任务、worker pool 中启动的 goroutine,都得各自设防
- 漏掉任何一个 goroutine 的 recover,就可能静默崩溃(尤其在后台任务中)
- 别指望中间件或全局 defer 能覆盖所有并发路径
recover 后程序不会回到 panic 发生点
很多人误以为 recover() 是“回滚”机制,其实它只是停止 panic 传播,并让控制流跳转到 defer 函数之后继续执行。panic 发生点之后的代码全部跳过,状态不会还原。
比如这段代码:
func riskyUpdate() {
mu.Lock()
defer mu.Unlock()
data = append(data, "new")
panic("oops") // 这行之后的代码不执行
data = append(data, "more") // ❌ 永远不会运行
}
即使你用 recover() 捕获了 panic,data 已被修改、“new”已追加,锁也已释放——但你没法撤回那一次 append。
- recover 不是事务回滚,它不保证数据一致性
- 如果 panic 前已修改全局变量、写入文件、发送网络请求,这些副作用无法撤销
- 真正需要原子性的操作,得靠显式锁、事务、或幂等设计,不能依赖 recover 补救
HTTP 中间件里的 recover 必须配合日志和堆栈
在 web 服务中用 recover() 防止进程退出是常见做法,但只写 http.Error(w, "500", 500) 很危险——你根本不知道发生了什么。
关键缺失项是堆栈信息。默认 recover() 只返回 panic 值(比如 "index out of range"),没有位置线索。必须手动抓取:
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("PANIC in %s: %v\n%s", r, string(buf[:n]))
http.Error(w, "Internal Server Error", 500)
}
}()
- 不打堆栈的日志等于没日志,下次复现困难
- 生产环境建议限制堆栈长度(避免 OOM),但至少保留前几帧
- 别把 panic 信息直接返回给客户端(安全风险),但服务端日志必须完整
recover 的真正价值不在“救活程序”,而在“让崩溃可追溯”。它解决的是故障可见性问题,不是逻辑健壮性问题——后者还得靠参数校验、error 返回、防御性编程。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











