应直接使用 github.com/sony/gobreaker,禁用手写状态机、归档的 afex/hystrix-go 及外层包裹 http.Get;必须实现自定义 RoundTripper 在 RoundTrip 中调用 cb.Execute 并透传 req 和 ctx;Timeout 设为下游 P95 耗时的 2–3 倍,ReadyToTrip 需按错误类型过滤,仅将超时类错误计入失败,降级逻辑须纯内存无副作用。

直接用 github.com/sony/gobreaker,别手写、别碰 afex/hystrix-go(已归档)、也别在外层套 cb.Execute 包裹 http.Get —— 这三种做法线上基本等于裸奔。
为什么必须包装 RoundTripper 而不是外层调用 Execute
HTTP 客户端的熔断粒度是「单次请求」,不是「整个 Client」。Go 的 http.Client 本身不提供拦截点,cb.Execute 外层包裹会绕过底层 Transport 的连接复用、超时透传和 context 控制。
- 超时失效:
context.WithTimeout无法穿透到net.Conn层,连接可能卡死几十秒 - 连接池泄漏:每次调用都新建
http.Client或跳过Transport.RoundTrip,FD 数暴涨 - 错误误判:
cb.Execute只捕获闭包 panic 和 error,但无法感知 HTTP 状态码(如 503)或区分net/url.Error和业务 404
正确做法是实现自定义 http.RoundTripper,在 RoundTrip(*http.Request) 内部调用 cb.Execute,并把原始 req 和 req.Context() 透传给底层 http.DefaultTransport.RoundTrip。
ReadyToTrip 和 Timeout 怎么配才不翻车
这两个参数默认值只适合本地调试,90% 的“熔断不生效”或“一直不恢复”都出在这儿。
立即学习“go语言免费学习笔记(深入)”;
-
Timeout(熔断持续时间):设为下游 P95 耗时的 2–3 倍;默认 60s 太长,下游服务已恢复,你的服务还在拒流 -
ReadyToTrip(触发条件):不要用默认的“100 次失败率 > 60%”。应显式过滤错误类型,例如只将context.DeadlineExceeded、net/http.ErrTimeout视为失败;sql.ErrNoRows、HTTP 404、401 必须放过 -
RequestVolumeThreshold(统计窗口最小请求数):低频服务(如管理后台接口)要调小(比如设为 5),否则永远达不到统计条件,断路器形同虚设
cb.Execute 闭包里传参的坑怎么避
这是线上最隐蔽的 panic 来源:循环中直接闭包捕获可变变量,导致所有请求共享同一内存地址。
错误写法:
for _, url := range urls {
req, _ := http.NewRequest("GET", url, nil)
cb.Execute(func() (interface{}, error) {
return http.DefaultClient.Do(req) // 所有闭包共用同一个 req 指针
})
}
结果是 http: Request.Write on Body closed 或请求错乱。
正确写法(显式传参):
for _, url := range urls {
req, _ := http.NewRequest("GET", url, nil)
cb.Execute(func(req *http.Request) func() (interface{}, error) {
return func() (interface{}, error) {
return http.DefaultClient.Do(req)
}
}(req))
}
更安全的方式是封装成独立函数,避免闭包捕获逻辑。
降级逻辑为什么必须纯内存、无副作用
cb.Execute 返回 gobreaker.ErrOpenState 时,你必须立刻返回可用结果,不能在此刻再去查 Redis、调配置中心、或发起另一个外部请求。
- 那等于绕过熔断保护,可能引发级联雪崩
- 降级响应应来自本地缓存、静态兜底数据、或上一次成功响应(带
staleheader 标识) - 任何依赖外部 IO 的降级,都会让断路器失去意义
真正难的从来不是加断路器,而是识别哪些错误该熔、哪些该放过,以及熔断后敢不敢真返回一个“假但可用”的结果。


















