降级需手动判断context.deadlineexceeded或context.canceled,用errors.is统一检测;降级函数须无副作用、类型严格匹配、禁用goroutine和远程调用;仅对瞬时故障降级,强一致性操作应熔断而非降级;开关必须动态可配且与熔断联动。

降级不是加个 fallback 就完事,得手动判断 context.DeadlineExceeded 和 context.Canceled
Go 没有自动降级机制,context.WithTimeout 只是让下游返回错误,不会帮你跳转逻辑。你必须在每次调用后立刻检查 err 是否属于上下文终止类错误。
- 只判
err == context.DeadlineExceeded是错的——上游主动 cancel 时会返回context.Canceled,漏掉它会导致本该降级的请求直接 500 - 别用
strings.Contains(err.Error(), "timeout"),不同 transport 错误文本不一致,不可靠 - 统一用
errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled),这是唯一健壮的写法 - gRPC 场景下,
status.FromError(err).Code()可能是codes.DeadlineExceeded,但底层仍是 context 错误包装而来,仍要走上面判断
降级函数必须无副作用,且返回值类型严格匹配主逻辑
降级不是兜底 panic,也不是补救错误,而是「主动选择次优路径」。一旦进了降级分支,就不能再发 HTTP、查 DB、改状态、打告警日志——这些都该在主流程失败后统一处理。
-
func GetUser(id string) (*User, error)的降级函数也必须返回(*User, error),哪怕error是nil - 避免返回零值结构体:比如
&User{}序列化后是{"name":"","age":0},前端可能当成真实空数据;改用DefaultUser()显式构造 - 别用全局变量存默认实例,goroutine 并发修改风险高;每次新建开销极小,别省这点内存
- 降级里禁止起 goroutine 或调另一个远程服务——这会让降级链失控,雪崩风险翻倍
不是所有错误都该触发降级,得区分 transient 和 permanent 错误
业务错误 ≠ 故障,乱降级等于掩盖问题。比如用户不存在(sql.ErrNoRows 或 gRPC codes.NotFound)是正常态,不该走兜底;而连接拒绝、IO 超时、codes.Unavailable 才是需要降级的瞬时故障。
- HTTP 客户端记得配
MaxIdleConnsPerHost和IdleConnTimeout,否则连接池耗尽会伪装成超时 - 数据库操作中,
sql.ErrNoRows不是错误,是预期结果;"dial tcp: i/o timeout"才要触发降级 - 对强一致性操作(如库存扣减、资金流水),别降级——应走熔断或重试,返回假数据比报错更危险
- 识别可降级点:查缓存代替查 DB、返回默认配置代替实时计算、调本地轻量服务代替远程重逻辑
降级开关必须可动态控制,且和熔断器联动
硬编码的降级等于没开关。上线后发现降级逻辑有 bug,或者下游已恢复,你的服务还在返回默认值——这就成了故障放大器。
- 别写
if isDegraded { return fallback() }这种静态 flag,要用配置中心或运行时开关(如原子布尔 + reload hook) - 推荐和熔断器联动:比如
gobreaker熔断打开时,自动启用降级;半开阶段可按比例放行主逻辑 - 降级逻辑本身也要有健康检查——比如缓存是否命中、本地配置是否加载成功,否则 fallback 也会失败
- 监控必须覆盖降级入口:记录降级触发次数、来源错误类型、fallback 命中率,否则你根本不知道它是不是真在起作用
最常被忽略的一点:降级和熔断是两层策略,不能互相替代。熔断是“停调用”,降级是“换路径”。一个接口既需要超时控制,又需要错误分类,还得配动态开关——三者缺一不可,少写一行就可能在线上出事。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











