Go协程panic不会自动传播,必须在每个goroutine内用defer+recover独立处理;主goroutine的recover对子协程无效;HTTP handler和守护协程是高频漏点,需特别防护。

Go 协程(goroutine)里的 panic 不会自动传播到主 goroutine,也不会终止整个程序——但若不显式 recover,该协程会静默退出,可能造成资源泄漏、连接未关闭、监控失察等隐蔽故障。必须在每个可能 panic 的 goroutine 内部独立处理。
goroutine 中必须自己加 defer + recover
主 goroutine 的 recover 对子 goroutine 完全无效;跨协程捕获是 Go 语言明确不支持的行为。你不能靠“外面包一层”来兜底子协程的 panic。
- ✅ 正确:每个
go func()启动时就自带defer func() { recover() }() - ❌ 错误:只在 main 函数里写一次
defer recover,以为能覆盖所有子协程 - ❌ 错误:把
recover()写在普通 if 判断里(不在defer中),它永远返回nil
典型写法:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v", r)
// 可选:runtime/debug.PrintStack()
}
}()
doRiskyWork() // 比如 json.Unmarshal 未知结构、反射调用、第三方库未校验输入
}()
safeGo 封装可复用,但别省略上下文和日志
重复写 defer+recover 易出错,封装成工具函数是合理选择,但生产环境不能只打一行日志就完事。
立即学习“go语言免费学习笔记(深入)”;
- 建议至少传入
*log.Logger,避免混用log.Default()和自定义 logger - 若业务有 trace ID 或 request ID,应一并注入日志,否则 panic 无法关联请求链路
- 不要在
recover块里再触发 panic(比如写日志时又因锁冲突 panic),会导致原始 panic 丢失
轻量封装示例:
func safeGo(f func(), logger *log.Logger) {
go func() {
defer func() {
if r := recover(); r != nil {
if logger != nil {
logger.Printf("panic in goroutine: %+v", r)
}
// 生产建议追加:debug.PrintStack()
}
}()
f()
}()
}
HTTP handler 和守护型 goroutine 是最常见漏点
这两类场景最容易忽略协程级 panic 防护,且后果严重:前者导致单个请求崩溃整个 server,后者让后台任务“悄无声息地消失”。
- HTTP handler 中,即使用了中间件
recover,若你在 handler 里另起 goroutine 处理耗时逻辑(如异步发通知),那个新 goroutine 仍需自己recover - 长周期守护协程(如定时拉取配置、监听 etcd、心跳上报)一旦 panic 且未 recover,就会永久退出,而进程仍在运行,监控却看不到异常
- 向已关闭的
chan发送数据、解码非法 JSON、空指针解引用,都是高频 panic 触发点,尤其在处理外部输入时
recover 后不能“继续执行”,只能清理和降级
很多人误以为 recover 能像 try-catch 那样“catch 住然后接着跑”。实际上,recover 只是停止 panic 传播,并让当前 goroutine 从 defer 函数返回后继续往下走——它不会回到 panic 发生的那一行代码。
- panic 后的变量状态不可信(比如部分字段已修改、锁未释放、文件句柄泄露)
- 不要在
recover后尝试重试原操作(如重连数据库),除非你确保状态完全干净 - 更安全的做法是:记录 panic、清理资源(
close、unlock)、然后退出该 goroutine,或由上层控制是否重建
例如,一个连接处理协程 panic 后,recover 应立即 c.Close(),而不是试图继续读写。
真正难的不是写对 defer+recover,而是判断哪些 goroutine “值得加”、哪些 panic “应该被 recover”、以及 recover 后要不要上报指标或触发告警——这些决策必须结合业务 SLA 和可观测性能力,不能只靠模板代码填满。










