main中recover无效,因recover仅对同goroutine生效;http handler、goroutine、定时任务等需各自添加defer+recover,且recover后应立即返回而非继续操作损坏状态。

单纯在 main 函数加 defer + recover,根本防不住服务崩溃——HTTP handler、goroutine、定时任务里的 panic 全都会直接杀进程。
为什么 main 里写 recover 没用
Go 的 recover 只对当前 goroutine 生效,而 Go HTTP server 启动每个请求时都会新建一个 goroutine。main 函数的 defer 只能捕获 main 自己 panic,对 handler 里 panic("nil pointer") 或 go func() { ... }() 里的崩溃完全无感。
- 现象:日志里看到完整 panic 堆栈,进程退出码为 2,k8s 持续重启 Pod
- 误区:以为“全局 recover”存在,或把
init()里注册 defer 当成兜底方案 - 本质:
recover不是异常处理器,它是 goroutine 级别的软着陆开关,必须和 panic 在同一个协程里配对
HTTP handler 必须各自加 defer + recover
不是加一次,是每个入口 handler 函数都得有。Gin 的 gin.Recovery() 之所以有效,是因为它被注入到每个 handler 执行链末尾,等价于你在每个 handler 开头手动写:
func myHandler(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
log.Printf("handler panic: %v", r)
c.AbortWithStatus(500)
}
}()
// 业务逻辑
}
- 别漏掉中间件、自定义路由组、WebSocket upgrade 后的连接处理函数
- 如果用了
http.ServeMux或net/http原生 handler,必须在每个http.HandlerFunc内部加 defer - Gin/Echo/Chi 等框架的 Recovery 中间件只是语法糖,不等于自动防护所有 goroutine
子 goroutine 必须自己包 defer,不能靠上游
任何显式用 go 启动的协程(如定时任务、消息消费、长连接心跳),都要在匿名函数第一行就注册 defer:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("worker panic: %v", r)
// 此处应触发熔断器 MarkFailed()
}
}()
for range ticker.C {
doWork()
}
}()
- 错误写法:
defer recover()—— 这会在注册时立刻执行,返回 nil - 错误写法:把 recover 提取到公共函数里再 defer 调用 —— recover 必须在 panic 发生的 goroutine 里调用
- 高频 panic 场景(如配置加载失败后仍继续运行)必须配合
srv.Shutdown(),否则 goroutine 泄漏+连接堆积比崩溃更危险
recover 后不能继续用已破坏状态
recover 成功只代表“没崩”,不代表程序状态还安全。数组越界、map 并发写、空指针解引用之后,内存结构可能已损坏。
- 禁止在 recover 块里继续
close(ch)、向疑似损坏的 map 写入、访问刚 panic 的 struct 字段 - 推荐模式:
if r := recover(); r != nil { log.Printf("panic: %v", r); return }—— 记录完立即退出函数 - 资源清理(如文件句柄、锁)必须放在独立的 defer 链里,而不是 recover 后的业务分支中
- 类型断言前必须检查
r != nil,panic(errors.New("x"))和panic("str")返回值类型不同,直接r.(error).Error()会二次 panic
真正容易被忽略的点是:recover 只解决“不挂”,但高可用的关键在于“挂了之后怎么快速止损”。一次 panic 后是否调用熔断器 MarkFailed()、是否触发 srv.Shutdown()、降级逻辑是否真的零依赖——这些动作漏掉任何一个,recover 就只是给崩溃裹了层糖纸。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











