最可控的Go HTTP重试方案是用net/http手动封装:在client.Do()外层实现指数退避重试,仅对网络错误或429/503等状态码重试,每次新建*http.Request,避免复用已读body。

用 net/http + 自定义重试逻辑最可控
Go 标准库本身不提供内置的 HTTP 重试机制,直接调用 http.DefaultClient.Do() 不会自动重试。想控制重试次数、间隔、失败条件,必须自己封装逻辑——不是加个第三方库才叫“可控”,而是从底层理解重试该在哪个环节介入。
关键点在于:重试应发生在请求发送失败(如连接拒绝、超时)或收到明确需重试的状态码(如 429、503)时,而不是对所有错误一概重试。
- 不要在
io.ReadFull或响应体解析阶段触发重试,那是业务逻辑错误,重试无意义 - 重试前建议检查
err类型:用errors.Is(err, context.DeadlineExceeded)或url.Error判断是否为网络层失败 - 避免对
400、401、404等客户端错误重试,它们不会因重试而变成功
http.Client 的 Transport 可以复用连接但不负责重试
http.Client 的 Transport 负责底层连接管理(如复用 TCP 连接、设置 MaxIdleConns),但它不会重试请求。即使你配置了 Transport.IdleConnTimeout 或 Transport.TLSHandshakeTimeout,也只是影响连接生命周期,和“请求失败后重新发一次”无关。
常见误解是以为设置了 Client.Timeout 就能触发重试——其实它只控制单次请求总耗时,超时后返回 context.DeadlineExceeded 错误,是否重试完全由上层决定。
立即学习“go语言免费学习笔记(深入)”;
- 重试逻辑必须写在调用
client.Do()的外层,比如 for 循环或递归函数中 - 每次重试应新建
*http.Request,不能复用已执行过的 request(body 可能已被读取) - 若 request body 是
io.Reader(如文件流),需确保能重复读取,否则要用bytes.NewReader或strings.NewReader包装
指数退避 + 最大重试次数的典型实现
硬编码固定间隔(如每次等 1 秒)容易打爆下游服务;线性增长也不够稳健。生产环境推荐指数退避(exponential backoff),配合 jitter 避免请求洪峰。
func doWithRetry(client *http.Client, req *http.Request, maxRetries int, baseDelay time.Duration) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i <= maxRetries; i++ {
resp, err = client.Do(req)
if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
return resp, nil
}
if i == maxRetries {
break
}
// 只对网络错误和部分服务端错误重试
if !shouldRetry(err, resp) {
return resp, err
}
delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(i)))
jitter := time.Duration(rand.Int63n(int64(delay / 2)))
time.Sleep(delay + jitter)
// 重建 request:防止 body 已被消耗
req, err = http.NewRequest(req.Method, req.URL.String(), req.Body)
if err != nil {
return nil, err
}
req.Header = req.Header.Clone()
}
return resp, err
}-
shouldRetry需判断:err != nil且属于网络错误;或resp != nil && (resp.StatusCode == 429 || resp.StatusCode/100 == 5) - 第 0 次是首次请求,所以循环上限是
i ,总共尝试 <code>maxRetries + 1次 - 用
req.Header.Clone()和重建*http.Request是为了安全,尤其当原始 request body 是非可重放类型时
用 github.com/hashicorp/go-retryablehttp 算省事但要注意配置粒度
这个库封装了标准 http.Client,支持自定义重试策略、状态码白名单、backoff 函数。但它默认对所有 5xx 都重试,包括 501 Not Implemented——这显然不该重试。
真正要用好它,必须覆盖默认行为:
- 通过
retryablehttp.LeveledClient设置Logger,否则重试过程静默,出问题难排查 - 用
CheckRetry显式控制哪些错误/状态码允许重试,别依赖默认值 -
RetryWaitMin和RetryWaitMax控制退避区间,但实际等待时间由Backoff函数计算,要确认它是否启用 jitter - 它内部会 clone request,但如果你传入的是带 streaming body 的 request(如上传大文件),仍可能出错——此时不如手写更清晰
重试的边界往往不在代码行数,而在你是否清楚每一次重试的理由。网络抖动?服务临时不可用?还是上游限流?这些判断没法交给通用库替你做。


















