recover() 只在 defer 中且同 goroutine 发生 panic 时有效,普通函数调用无法捕获;正确做法是将 defer recover() 置于可能 panic 的函数内部,或封装为 safego/safecall/http 中间件等带 defer 的执行模板。

为什么不能直接用 recover() 包裹任意函数
Go 的 recover() 只在 defer 中且当前 goroutine 发生 panic 时才有效,如果写成普通函数调用(比如 safeRun(fn) 内部调用 recover()),它根本捕获不到 panic——因为此时已不在 panic 的调用栈中。这是最常踩的坑:以为封装了就“安全”,结果 panic 照样飞出。
正确做法必须把 recover() 放进同一层 defer,且该 defer 必须注册在 panic 可能发生的函数内部或其调用链顶端。
- 不要写
func safeRun(f func()) { defer recover(); f() }—— 这完全无效 - 必须让
defer recover()和可能 panic 的代码处于同一个函数作用域内 - 若要封装,只能封装“带 defer 的执行模板”,而非“recover 调用本身”
safeGo:带恢复的 goroutine 启动器
并发场景下最需要 panic 恢复,否则一个 goroutine panic 会终止整个程序。这时可封装一个 safeGo 函数,它启动 goroutine 并内置 defer recover()。
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered in goroutine: %v", r)
}
}()
f()
}()
}
注意点:
- 必须用匿名函数包裹,否则
defer会在safeGo返回时执行,而不是在 goroutine 内部 - 恢复后仅记录日志不重抛,避免掩盖问题;如需上报,这里加 Sentry 或 Prometheus 指标上报逻辑
- 不建议在
safeGo里尝试“继续执行”或返回错误,goroutine 已中断,上下文丢失
safeCall:同步调用 + 错误返回的封装
对同步函数,更实用的是把 panic 转为 error 返回,便于上层统一处理。这需要闭包捕获和类型断言。
func safeCall(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
switch x := r.(type) {
case string:
err = fmt.Errorf("panic: %s", x)
case error:
err = fmt.Errorf("panic: %w", x)
default:
err = fmt.Errorf("panic: %v", x)
}
}
}()
f()
return nil
}
使用示例:
if err := safeCall(func() {
json.Unmarshal([]byte(`{`), &v) // 可能 panic
}); err != nil {
// 处理 panic 转来的 error
}
- 别用
interface{}直接转error,Go 的 panic 值类型不确定,必须做类型判断 - 这个封装只适合明确知道“该函数可能 panic 且你愿意吞掉它”的场景,比如解析不可信输入、调用第三方不稳定的库
- 不要滥用——正常错误应走
error返回,不是靠 panic 拦截
HTTP handler 中的 panic 恢复最容易漏掉的地方
很多人给 http.HandleFunc 加了 recover,却忘了中间件链或路由分发器本身也可能 panic(比如空指针解引用在 next.ServeHTTP 前)。真正安全的做法是恢复发生在最外层的 ServeHTTP 入口。
- 标准
http.ServeMux不自带恢复,必须自己包装http.Handler - 用
http.HandlerFunc包一层,defer recover()放在ServeHTTP方法内,不是放在每个 handler 函数里 - 恢复后应返回 500,并记录完整 panic stack(用
debug.PrintStack()或runtime.Stack()) - 注意:恢复后连接可能已半关闭,避免再写 response body
复杂点在于,panic 可能发生在中间件、handler、甚至 WriteHeader 之后;真正健壮的 HTTP 恢复必须在连接生命周期最外层控制,而不是靠某个函数封装解决。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











