go中recover只能捕获当前goroutine的panic,无法跨协程生效;http handler、子goroutine等必须各自显式添加defer+recover,main中recover无法兜住其他上下文panic。

Go 里没有 try-catch,recover 只能捕获当前 goroutine 的 panic
这是最常被误解的一点:很多人以为在 main 函数里 defer + recover 就能兜住所有 panic。实际不行——HTTP handler、定时任务、goroutine 启动的子任务,一旦 panic,主 goroutine 完全感知不到,服务直接挂掉或连接堆积。
真正有效的全局捕获,必须在每个可能 panic 的执行上下文里单独加 recover。比如:
-
http.HandlerFunc内部要 wrap 一层 recover - 启动新 goroutine 前,必须用匿名函数包裹并 defer
recover - 第三方中间件(如 Gin、Echo)提供
recovery中间件,本质也是在 handler chain 末尾插入 recover 逻辑
Gin 框架下启用 gin.Recovery() 不等于熔断
gin.Recovery() 只负责把 panic 转成 HTTP 500 响应,并打印日志,它不记录失败频次、不暂停后续请求、也不触发降级逻辑——这离“熔断”差得远。
要实现熔断,得自己叠加状态管理。常见做法是配合 gobreaker 库(或自研计数器),关键点有:
- 熔断器状态(Closed / Open / Half-Open)必须跨 goroutine 共享,推荐用
sync/atomic或sync.RWMutex保护 - 每次请求前检查熔断器状态,
breaker.Allow()返回 false 就直接走降级(fallback)或返回错误 - panic 发生后,除了 recover,还要显式调用
breaker.MarkFailed(),否则熔断器无法累积失败
示例片段:
func handleUserRequest(c *gin.Context) {
if !breaker.Allow() {
c.JSON(429, gin.H{"error": "service unavailable"})
return
}
defer func() {
if r := recover(); r != nil {
breaker.MarkFailed()
log.Printf("panic recovered: %v", r)
c.JSON(500, gin.H{"error": "internal error"})
}
}()
// real logic here...
}
HTTP server 的 Shutdown 和 panic 恢复必须协同
仅靠 recover 捕获 panic 并不能防止连接中断或资源泄漏。如果 panic 频发,服务可能已处于不稳定状态,此时该做的是快速止损,而不是硬扛。
建议在 recover 后触发优雅关闭流程:
- 设置一个全局
sync.Once控制只关一次 - 调用
srv.Shutdown()停止接收新请求,同时等待活跃连接完成 - 避免在 recover 里直接
os.Exit(),否则 systemd 或 k8s 会认为是 crash,反复重启 - 可搭配健康检查端点(如
/healthz)返回 503,让上游 LB 自动摘除节点
熔断阈值和超时配置容易被写死
很多团队把失败率阈值(比如 50%)、最小请求数(比如 20)、半开等待时间(比如 60s)硬编码在代码里。这会导致上线后无法动态调整,出问题时只能发版。
更合理的做法是:
- 从环境变量或配置中心加载熔断参数,例如
os.Getenv("CIRCUIT_BREAKER_FAILURE_RATE") - 使用
time.ParseDuration(os.Getenv("CB_HALF_OPEN_TIMEOUT"))解析持续时间 - 对关键服务,允许通过 HTTP POST 到
/admin/circuit-breaker/config实时更新(需鉴权)
注意:gobreaker.NewSettings 是不可变对象,每次修改都要重建熔断器实例,并替换旧引用——这点常被忽略,导致配置更新无效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











