recover仅在defer函数内调用且panic前注册才有效;跨goroutine无法捕获;须传error类型值以对接日志监控;recover后不可继续业务逻辑,仅能清理或降级。

recover 只在 defer 函数内部调用才有效,且必须在 panic 触发前注册;跨 goroutine 无法捕获,传 error 类型值才能对接日志与监控系统。
recover 必须写在 defer 匿名函数里,不能直接调用
这是最常踩的坑:把 recover() 放在普通代码块、if 分支或 panic() 后面,它永远返回 nil。因为 recover() 只在“栈正在展开、且当前 goroutine 处于 panic 状态”时才有意义,其他时机调用纯属无效操作。
- ❌ 错误写法:
func f() { panic("x"); r := recover() }——recover()根本不会执行 - ❌ 错误写法:
func f() { panic("x"); defer func() { r := recover() }() }——defer注册太晚,panic 已发生,栈已开始展开,注册无效 - ✅ 正确写法:
func f() { defer func() { if r := recover(); r != nil { log.Print(r) } }(); panic("x") }——defer在 panic 前注册,且recover()是该匿名函数体内的直接调用
子 goroutine 的 panic 无法被外层 recover 捕获
每个 goroutine 有独立的 panic/recover 上下文。主线程写了 defer + recover,对 go func() { panic("x") }() 完全无感——子 goroutine 会直接崩溃退出,不通知、不传播、不触发父级 defer。
- 典型翻车场景:HTTP handler 启动 worker goroutine 处理请求,worker 里 panic,但 handler 的 recover 拦不到
- 解决办法:每个可能 panic 的 goroutine 内部必须自己加
defer func() { recover() }() - 封装
safeGo(fn)时,必须确保fn内部也受保护,或在封装体内部完成 recover,例如:go func() { defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r) } }(); fn() }()
panic 传 error 类型值,别传裸字符串
虽然 panic("db timeout") 能跑,但线上出问题时你会失去结构化日志能力、字段扩展能力、监控系统自动识别能力。裸字符串无法携带错误码、trace ID、上下文参数等关键信息。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 推荐写法:
panic(errors.New("db timeout"))、panic(fmt.Errorf("failed to parse %s: %w", input, err)) - ✅ 更优写法:自定义错误类型,实现
Error()或String()方法,便于统一格式和序列化 - ⚠️ 注意:
recover()拿到的值就是你panic()传进去的原值,所以传什么,就得到什么——传字符串,r就是string;传error,r就是error接口,可直接用于log.Error(r)或 Sentry 上报
recover 后不能继续业务逻辑,只能做清理或降级响应
recover() 成功后,goroutine 并不会回到 panic 那一行重试,而是从 defer 函数返回后继续执行后续代码。此时局部状态大概率已损坏:map 可能处于中间态、指针可能已解引用失败、DB 连接可能已断开。
- ❌ 危险操作:在 recover 分支里再次调用
db.Exec()、http.Post()或修改共享变量 - ✅ 合理做法:记录完整堆栈(用
debug.Stack())、打日志(带 request ID)、返回 HTTP 500 或 fallback 响应、显式return - ⚠️ 特别注意:
recover()不回滚任何副作用,它不是事务机制,也不是重试开关,只是提供一次“软着陆”的机会


















