单纯在main加defer+recover完全无效,因HTTP handler在独立goroutine中运行,panic不跨协程传播;必须在每个handler入口或中间件最外层显式添加defer+recover,并为所有子goroutine单独封装safeGo以兜底捕获。

单纯在 main 函数加 defer+recover 完全无效——HTTP handler 是独立 goroutine,panic 不会跨协程传播,必须在每个请求入口处捕获。
HTTP handler 内必须显式加 defer+recover
Go 的 HTTP server 每个请求都在新 goroutine 中执行,main 里的 recover 对它完全无感。框架(如 Gin 的 gin.Recovery())或自定义中间件的本质,就是在每个 handler 执行前插入一个 defer func() { recover() }()。
- 错误写法:
main里写一次defer recover,以为能兜住所有请求 → 实际上 handler panic 后进程直接退出 - 正确姿势:每个 handler 函数开头就注册
defer func() { if r := recover(); r != nil { /* 记录、返回500、清理 */ } }() - 如果用了中间件(如
http.HandlerFunc链),recover必须放在链的最外层,且确保所有分支路径都经过它(比如if/else分支里不能漏掉 handler 调用)
goroutine 泄漏比 panic 更危险,必须各自加 recover
你在 handler 里启了个 go func() { ... }() 做异步日志或清理?它一旦 panic,既不会被 handler 的 recover 捕获,也不会打日志、更不会告警——只默默泄漏。
- 所有显式启动的 goroutine(包括定时任务、WebSocket 连接管理、后台轮询)都必须自己包
defer func() { recover() }() - 别依赖“反正有中间件兜底”——中间件只管 HTTP 请求 goroutine,不管你的子协程
- 建议封装一个安全启动函数:
safeGo(func() { ... }),内部自动加defer recover并记录堆栈(用debug.Stack())
recover 后不能“假装没事”,必须做三件事
recover 只是停止 panic 传播,不修复状态。继续用已 close 的 channel、已释放的 DB 连接、或未 rollback 的事务,大概率二次 panic 或数据错乱。
三层自动备份:每日时间戳快照、次级硬盘镜像、紧急对话导出。
立即学习“go语言免费学习笔记(深入)”;
- 记录完整堆栈:
log.Printf("panic recovered: %v\n%s", r, debug.Stack()) - 清理关键资源:关闭打开的文件、
close传出 channel、显式rollback数据库事务 - 明确返回错误:HTTP handler 中调
http.Error(w, ..., 500);RPC handler 返回status.Error(codes.Internal, ...),绝不能吞掉错误后继续返回业务数据
recover ≠ 熔断,失败计数必须手动触发
很多团队把 recover 当成熔断开关,结果服务高频 panic 却还在接新请求,连接堆积、内存暴涨。真正的熔断需要状态机和失败计数。
- 每次
recover捕获 panic 后,必须立刻调用熔断器的MarkFailed()(如gobreaker.NewCircuitBreaker(...).MarkFailed()) - 入口处必须先过
breaker.Allow(),返回false就直接降级或返回429,不能放行到业务逻辑再 panic - 降级逻辑必须零依赖:fallback 里禁止发 HTTP/DB 请求,只能读本地缓存或返回静态值
最容易被忽略的是:recover 后若不配合优雅关机(srv.Shutdown()),高频 panic 往往意味着服务已不可信,硬扛只会让问题雪球越滚越大。别等 k8s 反复重启,用 sync.Once 控制只触发一次 Shutdown,并设好 10–30 秒超时。

















