gin默认recovery中间件会吞掉panic且不调用c.error(),导致熔断器无法统计失败;需改用gin.new()并自定义recovery,在recover后调用c.error()并置状态,熔断器前置检查状态并abort。

为什么 Gin 默认 Recovery 会破坏熔断器的断路判断
Gin 的 gin.Default() 自带 Recovery() 中间件,但它在 panic 发生后直接返回固定字符串 {"error": "Internal Server Error"},且不透传 panic 值给后续中间件。这意味着你写的熔断器(比如基于错误计数的 circuitbreaker)根本收不到 panic 信号——它只监听 c.Error() 或显式 return 错误,而默认 Recovery 把 panic “吞掉”了,还提前终止了中间件链。
常见错误现象:熔断器统计不到失败次数,一直维持 closed 状态,下游服务已雪崩,它还在疯狂转发请求。
- 默认
Recovery()不调用c.Error(),也不触发c.Abort()以外的任何状态变更 - 你在熔断器中间件里写的
defer func() { if err := recover(); err != nil { cb.IncFailure() } }()完全无效——因为 panic 已被前面的 Recovery 拦截 - 必须用
gin.New()启动,并把自定义 Recovery 放在熔断器之前(顺序很重要)
如何让 panic 触发熔断器的 open 状态
核心是:让 panic 变成可被熔断器感知的“错误事件”。不能依赖 recover() 后静默处理,而要主动通知熔断器。
- 自定义 Recovery 中间件里,在
recover()捕获到 panic 后,立刻调用c.Set("panic", r),再调用c.Error(fmt.Errorf("panic: %v", r)) - 熔断器中间件放在 Recovery 之后(注意顺序),在
c.Next()后检查c.Errors.ByType(gin.ErrorTypePrivate)或读取c.Get("panic") - 避免在 Recovery 里直接调用
c.AbortWithStatusJSON()就结束——得先让熔断器有机会执行IncFailure() - 示例关键片段:
func Recovery(cb *circuit.Breaker) gin.HandlerFunc { return func(c *gin.Context) { defer func() { if r := recover(); r != nil { c.Set("panic", r) c.Error(fmt.Errorf("panic: %v", r)) // 不在此处 abort,留给后续中间件处理 } }() c.Next() } }
熔断器中间件中怎么区分 panic 和业务错误
仅靠 c.Error() 不够,因为业务层也可能调用它。得结合上下文标记或 panic 类型判断。
- 在 Recovery 中写
c.Set("is_panic", true),熔断器里用isPanic, _ := c.Get("is_panic")判断 - 或者检查
c.Errors中最后一个 error 的 message 是否含"panic:"前缀(简单但不健壮) - 更稳妥的方式:统一用自定义 error 类型,比如
type PanicError struct{ Value interface{} },在 Recovery 中c.Error(&PanicError{r}) - 性能影响:
c.Get()是 map 查找,开销可忽略;但频繁创建 error 实例可能增加 GC 压力,建议复用 error 对象或用 sync.Pool
断路后如何避免 Gin 继续执行下游逻辑
熔断器判定为 open 状态后,必须立即中断整个中间件链,否则仍会走到数据库、RPC 调用等耗时环节。
- 在熔断器中间件中,
c.Next()前就检查状态:if cb.State() == circuit.StateOpen { c.AbortWithStatusJSON(503, gin.H{"code": 503, "msg": "service unavailable"}) } - 不要等到
c.Next()执行完再判断——那时可能已经发起 RPC 请求了 - 注意:如果用了
c.AbortWithStatusJSON(),后续中间件和 handler 都不会执行,但前置中间件(如日志)的后置逻辑仍会跑,所以日志中间件要加if !c.IsAborted() { ... }保护 - 容易踩的坑:把熔断器中间件注册在 Recovery 之后,但又没在
c.Next()前做状态检查,等于白加
真正起作用的断路不是靠“捕获 panic”,而是靠“在请求进入业务前拦截”。panic 只是触发条件之一,关键在熔断器的状态决策点是否足够靠前、是否能阻断后续执行流。很多团队卡在这里:写了熔断器,却还在 panic 后查数据库。











