Go语言http.Client完全不提供自动重试,必须手动封装;推荐用backoff.Retry实现可控重试,注意错误分类、body可重放、请求头一致性及context取消。

Go 语言标准库的 http.Client **完全不提供自动重试**——无论你设了 Timeout、Transport 还是 CheckRedirect,遇到网络超时、502、DNS 失败等临时错误,它都直接返回错误,不会重发一次请求。
为什么不能只靠 http.Client.Timeout 或 Transport 参数
http.Client.Timeout 只控制单次请求总耗时,超时后返回 context.DeadlineExceeded 或 net/http: request canceled,不重试;Transport 的 MaxIdleConnsPerHost 等参数只管连接复用,和失败重发无关。误以为这些能“触发重试”,是线上故障最常见的认知偏差之一。
- 真正该重试的错误:
context.DeadlineExceeded(上层超时)、net.OpError(含"i/o timeout"、"connection refused")、HTTP 状态码500/502/503/504、429、408 - 绝对不该重试的错误:
400、401、403、404、405—— 这些是客户端语义错误,重试只是放大问题 - 别用
strings.Contains(err.Error(), "timeout")判断错误类型,要用errors.As(err, &netErr)或errors.Is(err, context.DeadlineExceeded)
用 backoff.Retry 封装一次可重试的 http.Do
手动写 for + time.Sleep 容易漏掉 context 取消、抖动、状态隔离,推荐用 github.com/cenkalti/backoff/v4(v4 支持 Go modules 且修复了 context 取消传播)。
- 把
http.Do包进一个无参函数里,返回error;backoff.Retry在 error 非 nil 时按策略重试 - 必须用
backoff.WithContext(b, ctx),否则req.Context().Done()无法中断重试循环 - 每次重试前调
req.Clone(ctx),否则req.Body已关闭或读空,第二次Do会报http: Request.Body is nil -
backoff.NewExponentialBackOff()默认MaxInterval = 128 * time.Second,对 API 调用太长,要显式设b.MaxInterval = 1 * time.Second
*http.Request.Body
- 推荐方案:把原始 payload 存为
[]byte,每次重试时用bytes.NewReader(payload)新建 body - 避免用
strings.NewReader("...")直接传字符串——中文或特殊字符可能因编码出错 - 如果 payload 很大(>1MB),别反复拷贝字节切片,改用支持
Seek的bytes.Reader或自定义可重放io.ReadCloser - 非幂等请求(如下单)重试前,务必和服务端约定幂等机制(如
Idempotency-Keyheader)
封装成 RetryClient 时最易忽略的细节
结构体字段设计比方法实现更重要——暴露太多内部细节会导致误用和语义污染。
- 别把
*http.Client嵌入结构体并导出;应只暴露Do(*http.Request) (*http.Response, error)方法 -
MaxRetries建议默认设为3,最大不超过5;设太大容易掩盖真实服务故障 - 每个请求必须用独立的
backoff.BackOff实例(调b.Reset()),不能多个 goroutine 共享一个实例,否则退避间隔错乱 - 重试日志建议用
backoff.RetryNotify,在通知回调里打,避免在重试函数里混入副作用逻辑
真正的难点不在“怎么写重试”,而在“什么时候不该重试”——错误分类不准、body 不可重放、context 生命周期失控、退避状态共享,这四点踩中任意一个,都会让重试从兜底变成雪崩。











