必须用高阶函数封装请求逻辑,因为http.Client.Do仅执行I/O,不处理超时、熔断、重试等可靠性逻辑;散落各处导致重复、脆弱、难维护,而高阶函数将「请求行为」抽象为func() (T, error),由统一策略层控制执行时机与方式,业务代码专注结果而非容错。

直接用 http.Client 发请求,不套高阶函数封装,就等于把超时、熔断、重试逻辑散落在各处——改一处漏三处,出问题难定位,根本没法维护。
为什么必须用高阶函数包装请求逻辑
因为 http.Client.Do 本身只做 I/O,它不关心失败是否可重试、当前下游是否已熔断、这次超时该不该影响下一次。把这些判断塞进业务 handler 里,会导致:每个接口都重复写 context.WithTimeout + gobreaker.Execute + backoff.Retry;错误类型判断靠 strings.Contains(err.Error(), "timeout") 这种脆弱匹配;超时时间硬编码在调用点,无法按下游服务 SLA 动态调整。
- 高阶函数本质是把「请求行为」抽象成
func() (T, error),再由统一策略层决定是否执行、何时重试、要不要跳过 - 它让业务代码只专注「我要什么」,而不是「怎么扛住抖动」
- 所有可靠性逻辑(熔断状态检查、指数退避计时、context 新建)都收口在一层,改策略不用动业务
如何用 gobreaker + context.WithTimeout 组合实现「带超时的熔断执行」
gobreaker.CircuitBreaker.Execute 接收的是 func() (interface{}, error),但它本身不处理超时——超时必须在被包装的函数内部显式控制,否则熔断器会等你卡死才报错。
- 每次调用前必须新建
context.WithTimeout,不能复用上游传入的 ctx:避免前一次超时污染本次重试 - 必须用
http.NewRequestWithContext构造请求,client.Do(req)才会响应 cancel - 错误返回前要判断
errors.Is(err, context.DeadlineExceeded),而不是err != nil就扔给熔断器——否则 DNS 解析失败和用户主动取消都会被当成下游故障计数 - 熔断器的
Timeout字段(如5 * time.Second)是「open 状态持续时间」,和单次请求的context.WithTimeout完全无关,别设成同一值
重试策略必须和错误类型强绑定
不是所有错误都值得重试:404 或 json.UnmarshalTypeError 重试一百次也没用,反而拉高失败率触发熔断。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 只对
net.OpError、http.ErrHandlerTimeout、5xx状态码重试,其他错误直接返回 - 用
cenkalti/backoff/v4.NewExponentialBackOff(),起始间隔100ms,最大10s,并开启jitter - 每次重试都重新生成 backoff.NextBackOff(),不要用固定 sleep —— 否则大量请求会在下游恢复瞬间集中冲击
- 重试函数体里不能有阻塞操作(如
time.Sleep),否则会拖慢熔断器状态更新
客户端结构体封装的关键字段与陷阱
别在每个 handler 里 new 一个 gobreaker.CircuitBreaker,也别把 http.Client 和熔断器耦合成一个 struct 后到处传参。
- 按下游服务维度初始化熔断器:比如
cbUserSvc、cbPaySvc,而不是 per-client -
http.Client.Transport必须自定义,禁用Client.Timeout,用DialContext、TLSHandshakeTimeout、ResponseHeaderTimeout分阶段控时 - 结构体方法
Do(ctx context.Context, req *http.Request) (*http.Response, error)要透传 ctx,且内部完成全部可靠性逻辑(熔断检查 → 超时包装 → 指数重试) - 别忘了在
defer resp.Body.Close()前检查resp是否为 nil,否则 panic
最易被忽略的点是:熔断器统计的「失败」必须严格对应业务语义上的不可用——比如 HTTP 状态码 503 要算失败,但 429 可能只是限流,应重试而非计入熔断计数;这层判断必须写在高阶函数包装的最内层,而不是丢给 gobreaker 默认逻辑。

















