goroutine 中 panic 不会传播到主 goroutine,必须在同 goroutine 内用 defer+recover 捕获;推荐封装 gowithrecover 工具函数统一处理,支持 context 取消与错误通道上报。

goroutine 中 panic 不会传播到主 goroutine
Go 的 goroutine 是独立的执行单元,内部 panic 默认不会影响其他 goroutine,包括 main。这看似安全,实则危险——panic 被静默吞掉,错误日志缺失,问题难以定位。recover 必须在 defer 中、且必须在 panic 发生的同一 goroutine 内调用才有效,跨 goroutine 无法 recover。
用匿名函数封装 + defer recover 捕获 panic
最直接有效的做法是:每个可能 panic 的 goroutine 启动时,用匿名函数包裹业务逻辑,并在其中部署 defer + recover。这不是“装饰器”,而是明确的错误隔离边界。
常见错误是把 recover 放在外部函数里(比如在启动 goroutine 前 defer),那完全无效——recover 只对当前 goroutine 的 panic 生效。
示例:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panicked: %v", r)
// 可选:上报指标、触发告警、写入 error channel
}
}()
// 这里放可能 panic 的代码,比如 map 并发写、nil 解引用、切片越界等
doRiskyWork()
}()
- 必须是
func() { ... }()立即执行的匿名函数,不能只写func() { ... }(那只是函数值,没运行) -
defer必须在 goroutine 内部注册,不能依赖外层函数的 defer - 恢复后建议记录完整堆栈:
log.Printf("panic: %v\n%v", r, debug.Stack()),否则只看到 panic 值,难定位
统一 panic 处理入口:带 context 和错误通道的封装
手动写一堆 go func() { defer ... }() 容易漏、难维护。更稳健的做法是抽一个工具函数,比如 GoWithRecover,它接收工作函数、context 和可选的 error channel。
这样做的好处是:统一日志格式、支持取消(ctx.Done())、便于集中监控 panic 频率。
示例签名与调用:
func GoWithRecover(ctx context.Context, f func(), errCh chan// 使用
errCh := make(chan error, 10)
GoWithRecover(ctx, func() {
panic("something went wrong")
}, errCh)
- error channel 设为带缓冲(如
make(chan error, 10)),避免 recover 后发送阻塞导致 goroutine 泄漏 - 不要在 recover 后继续执行原逻辑(比如重试),除非你明确知道 panic 原因且可安全续行;多数情况应终止该 goroutine
- 如果业务本身已用
context控制生命周期,GoWithRecover内部可加select { case 提前退出
别忘了 HTTP handler 和定时任务这类隐式 goroutine
很多人只记得显式 go 启动的 goroutine,却忽略 http.HandleFunc 或 time.AfterFunc 内部也是新 goroutine。这些地方同样会 panic 逃逸,且往往更致命(比如整个 HTTP 请求 500 却无日志)。
标准库的 http.Server 默认不 recover,需自行包装 handler:
func RecoverHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("HTTP handler panic: %v", r)
}
}()
h.ServeHTTP(w, r)
})
}
<p>// 使用
http.Handle("/api/", RecoverHandler(myHandler))</p>
- 第三方 Web 框架(如 Gin、Echo)通常自带 recovery 中间件,但要确认是否启用、日志是否输出堆栈
-
time.Ticker或time.AfterFunc启动的回调也需同样处理,否则定时任务 panic 后就永远消失了 - 测试时故意触发 panic(如
panic("test")),验证日志是否真实写出、HTTP 是否返回 500 而非空白响应
真正容易被忽略的,是那些你以为“不可能 panic”的地方——比如调用外部 SDK 返回的 interface{} 强转失败、JSON unmarshal 时字段类型错配、甚至 fmt.Sprintf 里传了不匹配的占位符。只要没做防御性检查,它们都可能在某个 goroutine 里悄无声息地崩掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











