Go 的 http.Client 不支持自动重试,需手动实现;正确做法是调用层循环+指数退避,检查错误类型与5xx/429状态码,重建请求体并加随机抖动,生产环境推荐 backoff/v4 库。

Go 的 http.Client 本身不支持自动重试
直接调用 http.DefaultClient.Do() 或自定义 http.Client 发起请求时,网络超时、连接拒绝、5xx 响应等错误都会立即返回,不会重试。必须手动封装逻辑——这不是缺陷,而是 Go 倡导显式控制的设计选择。
常见误操作是把重试写在业务函数里反复调用 Do(),但忽略了连接复用、超时继承、上下文取消传播等问题。正确做法是封装一个带重试能力的 RoundTripper,或更实际地:在调用层用循环 + 指数退避控制重试时机。
- 重试前务必检查错误类型:
url.Error中的Err是否为net.OpError或context.DeadlineExceeded,避免对 4xx(如404、401)盲目重试 - HTTP 状态码需单独判断:
resp.StatusCode >= 500 || resp.StatusCode == 429才适合重试,400和403通常不该重试 - 每次重试都应基于原始
*http.Request重建(尤其是含 body 的 POST/PUT),否则可能因 body 已被读取而失败
用 time.Sleep() 实现简单指数退避
不需要引入第三方库也能快速落地。核心是用 math.Exp2(float64(attempt)) * baseDelay 计算等待时间,并叠加随机抖动(jitter)防止雪崩。
baseDelay := 100 * time.Millisecond
maxRetries := 3
<p>for i := 0; i <= maxRetries; i++ {
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 500 && resp.StatusCode != 429 {
return resp, nil
}
if i == maxRetries {
return nil, err
}</p><pre class="brush:php;toolbar:false;">// 指数退避 + jitter
sleep := time.Duration(float64(baseDelay) * math.Exp2(float64(i)))
sleep += time.Duration(rand.Int63n(int64(sleep / 2))) // ±50% jitter
time.Sleep(sleep)
// 重建 request(尤其 body 需重放)
req, _ = http.NewRequestWithContext(ctx, req.Method, req.URL.String(), req.Body)}
注意:req.Body 是 io.ReadCloser,不可重复读。若需重放 body,得提前用 io.ReadAll() 缓存并构造 bytes.NewReader(),或用 http.NoBody 替代空 body。
用 golang.org/x/time/rate 控制重试频率?不适用
rate.Limiter 是用于限流(如每秒最多 N 次请求),不是为单个请求的重试退避设计的。它无法感知某次请求失败后该等多久再试——退避时间取决于失败次数,而非固定速率。
强行套用会导致两个问题:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 重试间隔变成固定值(如每秒 1 次),失去指数增长特性
- 多个并发请求共享同一个
Limiter时,彼此阻塞,违背“每个请求独立退避”的原则
真正需要的是按尝试次数动态计算延迟,time.Sleep() 配合公式已足够轻量可靠。
生产环境建议用 backoff/v4 库统一管理
当项目中多处需要重试(数据库、HTTP、消息队列),硬编码逻辑会散落各处且参数不一致。推荐使用 github.com/cenkalti/backoff/v4,它提供标准退避策略和可组合的选项。
关键点:
- 用
backoff.WithMaxRetries(&backoff.ExponentialBackOff{}, 3)包装退避配置 - 调用
backoff.RetryNotify(),传入你的执行函数和错误处理回调 - 它自动处理 jitter、最大间隔(
MaxInterval)、总超时(MaxElapsedTime)等边界
唯一要注意的是:该库不感知 HTTP 上下文取消,所以你的执行函数内部仍需检查 ctx.Err() 并提前返回,否则可能在重试中途卡住。
退避逻辑本身不复杂,但组合超时、取消、body 重放、状态码判断后,手写容易漏掉某条路径。用成熟库省下的不仅是代码行数,更是排查 “为什么第 3 次重试没触发” 这类问题的时间。

















