用 backoff.Retry 封装 http.Do 时必须处理三件事:每次重试需 clone 请求体以防 Body EOF;显式传递上下文避免 ctx.Done() 失效;手动配置 MaxInterval 和 MaxElapsedTime 防止等待过久。

用 backoff.Retry 封装 http.Do 时必须处理的三件事
直接把 http.DefaultClient.Do 塞进 backoff.Retry 会失败,不是库有问题,而是 HTTP 请求本身有状态约束。
-
req.Body是io.ReadCloser,第一次Do后就 EOF 或关闭,第二次调用报http: Request.Body is nil;必须每次重试前调用req.Clone(req.Context()),或手动重建 body(如bytes.NewReader(bodyBytes)) - 原
req.Context()不会自动传播到重试循环里,漏掉backoff.WithContext(..., req.Context()),ctx.Done()就永远不生效 -
backoff.ExponentialBackOff默认MaxInterval = 128 * time.Second,对 API 场景太长,建议显式设为1 * time.Second,并配MaxElapsedTime控制总耗时上限
哪些错误该重试,哪些绝对不能碰
重试不是兜底,是针对临时性故障的补救。错判会导致雪崩或暴露敏感逻辑。
- 可重试:网络层错误(
net.OpError、context.DeadlineExceeded)、5xx 状态码、429、408 —— 这些大概率是服务端瞬时压力或调度问题 - 不可重试:4xx 中除 429/408 外的所有状态(如
401 Unauthorized、400 Bad Request、404 Not Found),它们代表客户端错误,重试只是刷日志 - 特别注意:某些
url.Error的Err字段含"EOF"或"connection refused"可重试,但"tls: bad certificate"这类明确配置错误不能重试
封装成 RetryClient 类型时要注意什么
直接写函数也能用,但多人协作或项目变大后,封装成结构体更可控。重点不是“怎么定义 struct”,而是字段设计是否暴露了不该暴露的细节。
- 别把
backoff.BackOff实例作为字段直接复用 —— 多个请求共用同一实例会导致NextBackOff()返回错乱间隔;要么每次新建,要么在方法内调b.Reset() -
CheckRetry函数必须可配置,不能硬编码;不同下游 API 对 429 的处理方式不同(有的带Retry-After,有的没),靠字段传入比写死更灵活 - 如果支持带 body 的请求(POST/PUT),
RetryClient必须要求调用方提供func() io.ReadSeeker而非原始io.Reader,否则无法重放 body
为什么不用 go-retryablehttp 而要自己封装
它确实省事,但默认行为在生产环境容易踩坑,不是它不好,而是开箱即用的假设太宽泛。
立即学习“go语言免费学习笔记(深入)”;
- 默认
CheckRetry对所有 4xx 都重试,线上曾因反复重试401触发对方风控限流 - 它内部用
http.Client,但默认Timeout = 30s,若你没显式替换Transport或设Timeout,真实超时可能被掩盖 - 当需要精细控制重试前清理(如重置自定义 header、注入 trace id)、或与现有 metrics / log 框架集成时,自己封装的函数更容易插桩
真正麻烦的从来不是“怎么写重试”,而是判断“这次失败到底该不该重试”——这个决策点没法抽象成通用逻辑,必须贴着业务接口文档来定。


















