过滤链必须按职责分层:外层日志与超时、中层限流与熔断、内层仅对瞬时错误(如超时、502/503/504)执行无依赖降级;顺序错误或暴露内部状态会导致降级失效或状态污染。

过滤链必须按职责分层,不能把降级逻辑塞进同一个中间件
一个中间件既做鉴权、又做限流、再加降级,等于把三件事绑死——某环节出问题,整条链就瘫。真实项目里,降级只该响应 transient error,而鉴权失败(401)或参数错误(400)根本不该走 fallback。常见错误是写成:if err != nil { return fallback(), nil },结果用户传错 ID 也返回兜底数据,掩盖了业务逻辑缺陷。
正确分层方式是:
- 最外层放日志和超时控制(
context.WithTimeout),确保所有后续环节有统一 deadline - 中间层放限流(
rate.Limiter.Wait)和熔断(gobreaker.CircuitBreaker.Execute),只包裹真正可能失败的下游调用,比如db.QueryRow或http.Client.Do - 最内层才放降级逻辑,且只对明确的 transient error 做响应:如
context.DeadlineExceeded、net.ErrClosed、HTTP 502/503/504
降级必须无外部依赖,且兜底数据要支持热更新
降级函数里再起 goroutine 调另一个 HTTP 接口,或者查主库,等于在雪崩时主动推一把。线上出问题时,降级路径本身必须稳——它只能读本地缓存、配置文件或预热好的 sync.Map。
实操要点:
- 兜底数据不能硬编码,比如
&User{Name: "default"};应从 JSON 文件加载,用fsnotify监听变更 - 关键字段要做校验,例如价格字段降级值不能为负,库存不能超上限,否则前端展示异常
- 用
sync.Map缓存最近生成的兜底结构体,避免每次请求都重新构造 - 降级函数签名必须返回
(*T, error),且error为nil——这是让上层业务逻辑继续执行的关键,不是“假装成功”
过滤链组装顺序决定降级是否生效
中间件顺序写反,降级永远触发不了。比如把熔断器放在超时控制之前,那 context 超时还没生效,熔断器就先报错了;或者把日志中间件放在最内层,那降级响应根本不会被记录。
标准链顺序应为:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
logging(记录起始)→ -
timeout(设 deadline)→ -
rateLimit(排队等令牌)→ -
circuitBreaker(包装下游调用)→ -
fallbackHandler(仅对 transient error 返回兜底)→businessHandler
验证方法:每个中间件开头加 log.Printf("in %s", name),看输出是否严格按此顺序;若发现 fallbackHandler 没打印,说明它被外层中间件提前终止了。
internal 包是隔离过滤链实现细节的唯一可靠方式
把过滤链的构建逻辑、熔断器实例、兜底数据加载器全丢进 pkg/filter 顶层包里?那就等于向所有模块暴露了内部状态。一旦其他包直接 import 并复用同一个 gobreaker.CircuitBreaker 实例,不同接口的熔断状态就会互相污染。
必须用 internal 切割边界:
-
pkg/filter/internal/chain:存放Chain类型、Middleware接口定义、组装逻辑 -
pkg/filter/internal/middleware:各中间件实现,含timeout.go、fallback.go等,不对外导出 -
pkg/filter/internal/fallback:兜底数据加载、校验、缓存逻辑,只通过pkg/filter.NewChain()对外提供工厂函数
这样,外部模块只能用 pkg/filter.Chain 和 pkg/filter.Option,无法触碰任何内部状态——降级是否自主,取决于你有没有守住这道 internal 的门。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










