Go标准库http.Client默认不重试,因重试需权衡幂等性、状态跟踪与退避策略;推荐用hashicorp/go-retryablehttp实现可控重试,支持错误类型过滤、指数退避及自定义重试判断。

为什么默认的 http.Client 不会自动重试
Go 标准库的 http.Client 在遇到网络错误(如 net/http: request canceled、connection refused)或超时(context.DeadlineExceeded)时,**直接返回错误,绝不重试**。这是设计使然——重试逻辑涉及幂等性判断、状态跟踪、退避策略等,标准库选择不越界。
如果你看到请求偶尔失败且没重试,不是配置漏了,而是本来就不支持。
用 retryablehttp 库实现可控重试(推荐方案)
社区最成熟的选择是 hashicorp/go-retryablehttp,它在保留原生 http.Client 行为基础上,封装了重试能力,且不侵入业务逻辑。
- 重试只对特定错误生效:默认重试网络层错误(
net.ErrClosed、net/http: TLS handshake timeout),但跳过 4xx(如404、401)和 5xx 中明确非临时性的响应(如501) - 指数退避可配:
RetryWaitMin和RetryWaitMax控制间隔,避免雪崩 - 可注入自定义判断逻辑:通过
CheckRetry函数决定某次响应是否值得重试,比如对503 Service Unavailable强制重试
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 100 * time.Millisecond
client.RetryWaitMax = 400 * time.Millisecond
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err != nil {
return true, nil // 网络错误一律重试
}
if resp.StatusCode == 503 || resp.StatusCode == 429 {
return true, nil // 明确重试限流/服务不可用
}
return false, nil // 其他状态码不重试
}
// 使用方式:client.StandardClient() 返回 *http.Client,可直接传给依赖 http.Client 的代码
http.DefaultClient = client.StandardClient()自己封装重试时,必须处理的三个关键点
手写重试看似简单,但漏掉任意一点都会导致静默失败、重复提交或 goroutine 泄漏。
立即学习“go语言免费学习笔记(深入)”;
-
Context 必须透传并重置:每次重试都要新建
context.WithTimeout或context.WithDeadline,不能复用原始 context —— 否则第一次超时后,后续重试立刻失败 -
请求 Body 需可重放:如果用了
strings.NewReader或bytes.NewReader,没问题;但若 Body 来自文件或管道(os.Stdin、io.PipeReader),无法二次读取,会导致后续重试发送空体 -
避免对非幂等请求重试:比如
POST /orders,重试可能创建多个订单。应在重试前检查 method + path,或依赖服务端返回的Retry-After+ 幂等 key(如X-Idempotency-Key)
连接池与超时配置直接影响重试效果
重试再完善,底层连接池耗尽或单次请求卡死,也会让重试失去意义。
-
Transport.MaxIdleConns和MaxIdleConnsPerHost建议设为 100+,尤其高并发调用同一域名时,否则新请求会阻塞在拨号阶段,看起来像“重试没生效” -
Transport.IdleConnTimeout设为 30s 左右,避免长连接被中间设备(NAT、LB)静默断开,导致下一次请求触发重连而非复用 -
Timeout、KeepAlive、TLSHandshakeTimeout都要显式设置,否则走默认 0(无限),重试逻辑可能永远等不到错误
tr := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
client := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}重试不是加个 for 循环就完事;真正难的是在失败信号、上下文生命周期、HTTP 语义、连接状态之间做精确协调。多数线上问题,出在退避策略太激进,或把 4xx 当成临时错误重试。











