必须使用 github.com/sony/gobreaker,禁用手写状态机、已归档的 afex/hystrix-go 及外层包裹 http.get;http 熔断须封装 roundtripper 并透传 req 和 ctx,timeout 设为下游 p95 耗时的 2–3 倍,readytotrip 需按错误类型过滤,降级逻辑须纯内存无副作用。

直接用 github.com/sony/gobreaker,别手写状态机、别碰已归档的 afex/hystrix-go,更别用 cb.Execute 外层包裹 http.Get——这三种做法在线上等于放弃熔断控制权。
为什么必须包装 RoundTripper 而不是套一层 Execute
HTTP 熔断的粒度是「单次请求」,不是整个 http.Client。Go 的 http.Client 没有拦截点,外层调用 cb.Execute 会绕过底层 Transport 的连接复用、超时透传和 context 控制:
-
context.WithTimeout无法穿透到net.Conn层,请求可能卡死几十秒 - 每次调用都新建连接或跳过
RoundTrip,FD 数暴涨、TLS 握手激增 -
cb.Execute只捕获闭包返回的error,但感知不到503状态码,也分不清net/url.Error和业务404
正确路径:实现自定义 http.RoundTripper,在 RoundTrip(*http.Request) 内部调用 cb.Execute,并透传原始 req 和 req.Context()。
ReadyToTrip 和 Timeout 怎么配才不误判
这两个参数默认值只适合本地调试,线上不调等于没开熔断:
-
Timeout(熔断持续时间):设为下游 P95 耗时的 2–3 倍;默认60 * time.Second太长,下游早恢复了你还拒流 -
ReadyToTrip(触发条件):不要用默认“100 次失败率 > 60%”。应显式过滤错误类型,例如只将context.DeadlineExceeded、net/http.ErrTimeout视为失败;sql.ErrNoRows、HTTP404、401必须放过 -
RequestVolumeThreshold(统计窗口最小请求数):低频服务(如管理后台)要调小(比如设为5),否则永远达不到统计条件
cb.Execute 闭包里传参的坑怎么避
这是线上最隐蔽的 panic 来源:循环中直接闭包捕获可变变量,导致所有请求共享同一内存地址:
- 错误写法:
for _, url := range urls { req, _ := http.NewRequest("GET", url, nil); cb.Execute(func() { client.Do(req) }) }→ 所有闭包共用同一个req指针 →http: Request.Write on Body closed - 正确写法:
cb.Execute(func(req *http.Request) func() (interface{}, error) { return func() { return client.Do(req) } }(req)) - 返回的
*http.Response必须带非nil的Body(哪怕用io.NopCloser(bytes.NewReader(nil))填充),否则上层resp.Body.Close()会 panic
降级逻辑必须纯内存、无副作用
降级不是兜底,而是故障时的确定性响应:
- 不能查 Redis、不能调配置中心、不能发新 HTTP 请求
- 不能启动 goroutine、不能加锁、不能依赖外部状态
- 返回兜底 JSON、静态文案,或带
staleheader的上一次成功响应即可 - 降级必须和
cb.Execute解耦:先执行,捕获gobreaker.ErrOpen,再走独立fallback分支;别把 fallback 塞进 Execute 闭包里
真正难的不是初始化一个 gobreaker.CircuitBreaker,而是把 RoundTrip 包装对、把错误分类清、把降级做薄——这三处任一出错,熔断就从保护变成阻塞。











