gobreaker.Execute 不自动触发 fallback,需手动捕获 gobreaker.ErrOpen 并显式调用纯函数降级;超时和错误判断须用 errors.Is;fallback 必须无副作用、类型匹配、可动态开关,并配套日志与监控。

gobreaker.Execute 里 fallback 必须手动写,不是自动触发
gobreaker 不会自动调用你的降级逻辑,它只决定“这次请求要不要执行”。一旦熔断器状态为 open,Execute 直接返回 gobreaker.ErrOpen,你得自己捕获并显式走 fallback 分支。
- 错误写法:把降级逻辑塞进
Execute的函数体里——这会导致即使熔断器关闭,每次调用也必走降级 - 正确流程:
result, err := cb.Execute(mainCall)→if errors.Is(err, gobreaker.ErrOpen)→ 手动调用fallbackUser() -
fallbackUser()必须是纯函数:不发 HTTP、不查 DB、不依赖外部状态;建议返回硬编码结构体或本地缓存值 - 别在 fallback 里调
time.Now()或rand.Intn()—— 这会让结果不可预测,破坏幂等性
context.WithTimeout 后必须同时检查 DeadlineExceeded 和 Canceled
只判断 err == context.DeadlineExceeded 是错的。gRPC、HTTP 客户端返回的超时错误可能被封装多层,errors.Is(err, context.DeadlineExceeded) 才可靠;而上游主动 cancel(比如网关超时)会触发 context.Canceled,漏掉它会导致降级失效,直接返回 500。
- HTTP 场景下,还要额外检查网络类错误:
strings.Contains(err.Error(), "i/o timeout")或"connection refused",这些不是 context 错误,但同样该降级 - gRPC 调用要用
status.Code(err)判断:codes.DeadlineExceeded、codes.Unavailable可降级;codes.NotFound是业务态,不该兜底 - 数据库操作中,
sql.ErrNoRows不是错误,不能触发降级;而"dial tcp: i/o timeout"才是
cache.Get 失效时用 singleflight.Group 防雪崩,但要过滤错误
多个并发请求同时发现缓存过期,若都去调下游,容易打垮依赖服务。用 singleflight.Group 能保证同 key 只有一个 goroutine 加载,但默认会把错误结果也广播给所有等待者。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须包装一层:只在
err == nil时才写入缓存,失败则让等待者各自走降级路径,避免“全量降级” -
singleflight.Do的 key 必须是 string;传 struct 或指针会导致相同业务语义被当成不同 key,重复加载 - 缓存 key 要含业务维度(如
"user:123:cn"),不能只用原始参数,否则地域/版本差异导致数据污染 - sync.Map 不支持 TTL,得自己加时间戳字段 + 惰性淘汰;bigcache 更适合带过期的场景
降级开关必须可动态控制,且 fallback 返回值类型严格匹配主逻辑
硬编码的降级等于没开关。线上发现降级逻辑有 bug,或者下游已恢复,你没法关——只能重启服务,这违背“优雅”本意。
立即学习“go语言免费学习笔记(深入)”;
- 把开关、超时阈值、fallback 内容存在 etcd 或 Nacos;每个 service 方法读配置前加 cache layer,避免高频拉取
-
func GetUser(id string) (*User, error)的降级函数也必须返回(*User, error),哪怕error是nil;否则编译不过 - JSON 序列化时,零值字段(如
Age: 0)易被前端误判;优先用*int类型,降级时返回nil,或定义DefaultUser()显式赋值 - 每个降级分支必须打日志 + metrics 打点,比如
metrics.Counter("user_service_fallback_total").Inc(),否则你根本不知道它是否生效

















