应直接使用 sony/gobreaker,它被 go-zero、kratos 和 Shopify 等高负载系统验证,线程安全、无依赖、原生支持 context.Context;手写易致并发统计失真、半开探测失控;HTTP 熔断须封装 RoundTripper 而非外层包 Execute;Timeout 应设为下游 P95 耗时的 2–3 倍,ReadyToTrip 需按错误类型过滤,降级逻辑必须显式捕获 gobreaker.ErrOpen 且纯内存无副作用。

直接用 sony/gobreaker,别手写状态机,也别用已归档的 hystrix-go。它被 go-zero、kratos 和 Shopify 等真实高负载系统长期验证,线程安全、无依赖、原生支持 context.Context,关键配置项(如 ReadyToTrip、Timeout)调对了就能稳住生产流量。
为什么必须用 gobreaker 而不是自己实现
手写熔断器看似简单,但高并发下极易出错:
- 多个 goroutine 同时触发
onFailure,计数器未用sync/atomic保护,统计失真 - 半开状态下不控制试探请求数量,刚恢复的服务瞬间被打垮
- 滑动窗口没做时间衰减或滚动重置,错误率计算偏离真实值
- 状态切换时机错乱,比如
Open → HalfOpen过早或过晚,导致误熔断或拒流过久
gobreaker 把这些细节全封装好了,单文件、零外部依赖,go get github.com/sony/gobreaker 就能用。
HTTP 客户端怎么正确包装熔断逻辑
错误做法是把整个 http.Client.Get() 套进 cb.Execute —— 这会破坏连接池复用、丢失 context 透传、超时无法中断底层请求。
立即学习“go语言免费学习笔记(深入)”;
正确方式是实现自定义 RoundTripper:
type CircuitBreakerTransport struct {
cb *gobreaker.CircuitBreaker
rt http.RoundTripper
}
<p>func (t CircuitBreakerTransport) RoundTrip(req <em>http.Request) (</em>http.Response, error) {
result, err := t.cb.Execute(func() (interface{}, error) {
return t.rt.RoundTrip(req)
})
if err != nil {
return nil, err
}
return result.(*http.Response), nil
}然后创建 client 时传入:
client := &http.Client{
Transport: CircuitBreakerTransport{
cb: cbUserSvc,
rt: http.DefaultTransport,
},
}- 所有通过该 client 发出的请求自动受控
-
context、超时、TLS 设置全部保留 - 避免在每个业务函数里重复写
cb.Execute,不易漏、好维护
关键配置项怎么设才不踩坑
gobreaker.Settings 里最容易配错的是 Timeout 和 ReadyToTrip:
-
Timeout不是请求超时,而是熔断打开后多久进入HalfOpen状态;建议设为下游服务 P95 耗时的 2~3 倍(比如 P95 是 400ms,这里设1200 * time.Millisecond) -
ReadyToTrip默认只看失败次数,但真实场景要过滤错误类型:比如net/http: request canceled、context.DeadlineExceeded算失败,而404或sql.ErrNoRows必须放过 -
MaxRequests控制半开时放行几个试探请求,设 3~5 较安全;设 1 恢复太慢,设 100 可能压垮刚恢复的下游 - 低频服务(QPS Requests 或改用时间窗口,否则永远达不到统计阈值
示例定制逻辑:
ReadyToTrip: func(counts gobreaker.Counts) bool {
total := counts.TotalSuccesses + counts.TotalFailures
if total < 10 {
return false
}
failureRatio := float64(counts.TotalFailures) / float64(total)
return failureRatio >= 0.3
}降级逻辑为什么不能藏在 Execute 闭包里
cb.Execute 的第三个参数才是 fallback,但它只在 gobreaker.ErrOpen 或主函数 panic 时触发。很多人把降级写成:
cb.Execute(func() (interface{}, error) {
// 主逻辑
}, func(err error) interface{} {
// 这里写降级
return getFallback()
})问题在于:如果主逻辑返回的是业务错误(比如 http 503),它会被计入失败统计,但不会进 fallback —— 熔断器根本不知道该降级。
正确做法是显式捕获 gobreaker.ErrOpen:
result, err := cbUserSvc.Execute(func() (interface{}, error) {
// HTTP 调用,显式将 5xx 转 error
})
if errors.Is(err, gobreaker.ErrOpen) {
return getFallbackUser(), nil // 纯内存兜底,不查 Redis、不发新请求
}
if err != nil {
return nil, err
}
return result.(User), nil- 降级必须是纯内存、无副作用、不依赖外部服务
- 不能嵌套 HTTP 请求或 DB 查询,否则等于绕过熔断保护
- HTTP 接口可返回上一次成功响应(加
staleheader 标识),或静态 JSON
最常被忽略的一点:熔断器只管“开关”,不管“超时”。context.WithTimeout 必须在调用前就加,且超时时间要略小于 Timeout,否则底层连接不释放,goroutine 会堆积。


















