在Go语言的实际开发中,HTTP请求重试是个绕不开的话题,但很多人在实现时容易走弯路。比较常见的一种做法是直接在http.Do()外面套一个for循环,简单粗暴,但副作用也很明显——丢了连接复用、超时控制和重定向这些http.Client内置能力。真正专业的做法,是把重试逻辑下沉到client层,复用http.Client,通过自定义Transport或中间层来拦截错误并触发重试。
重试逻辑必须封装在 client 层,别在业务代码里手写 for 循环
直接在 http.Do() 外套 for 循环看似简单,但会丢失连接复用、超时控制、重定向等 http.Client 原生能力。正确做法是复用 http.Client,通过自定义 Transport 或中间层拦截错误并重试。
关键点在于:重试应发生在请求发出后、响应解析前,且只对特定错误重试(如网络断连、5xx 服务端错误),不能对 4xx(比如 404、401)或 context.DeadlineExceeded 盲目重试。
- 使用
http.Client的Timeout和Transport的IdleConnTimeout配合控制整体耗时 - 重试间隔建议用指数退避(
time.Second * 1, 2, 4, 8...),避免雪崩 - 务必设置最大重试次数(如 3 次),否则可能卡死或触发限流
用 RoundTripper 包装 Transport 实现透明重试
最干净的方式是实现自定义 RoundTripper,把重试逻辑下沉到底层。它能拦截每次 RoundTrip 调用,判断是否需要重试,并复用原 Transport 发起新请求。
注意:不能直接修改原始 req.Body(它是 io.ReadCloser,读过即关闭),重试前需用 req.GetBody() 重建可重放的 body —— 这要求你在初始化 http.Request 时已设置 req.GetBody,或用 bytes.NewReader + bytes.Buffer 预缓存。
- 只有实现了
req.GetBody的请求才支持重试(strings.NewReader、bytes.NewReader可自动支持;os.File不行) - 重试时需克隆
*http.Request(用req.Clone(req.Context())),否则 header、body 等状态会污染 - 不要在重试逻辑里修改原始
req.Header,应在 clone 后操作
第三方库选型:github.com/hashicorp/go-retryablehttp 更靠谱
自己实现容易漏掉边界情况(如 TLS 握手失败、DNS 解析失败、HTTP/2 stream error)。github.com/hashicorp/go-retryablehttp 封装了成熟策略:默认对 net.Error、url.Error、5xx 状态码重试,跳过 4xx 和重定向响应,并内置指数退避和 jitter。
它返回的是标准 *http.Client,可直接替换原有 client,兼容所有下游调用:
client := retryablehttp.NewClient() client.RetryMax = 3 client.RetryWaitMin = time.Second client.RetryWaitMax = time.Second * 5 httpclient := client.StandardClient() // 返回 *http.Client resp, err := httpclient.Do(req)
- 它不重试
context.Canceled,但会重试context.DeadlineExceeded(这点要留意,若你用短 context 控制单次请求,需关掉重试或改用自定义策略) - 不支持对部分路径或 method 单独配置重试,如需差异化策略,得自己 wrap
RoundTripper - 日志需通过
client.Logger注入,不打日志时设为nil避免 nil panic
重试时最容易被忽略的 Header 和 Cookie 问题
很多接口依赖 X-Request-ID、Authorization 或 session cookie,重试时若没正确继承这些字段,会导致重复提交、鉴权失败或状态不一致。
标准 req.Clone() 会复制全部 header 和 cookie,但要注意两点:
- 如果用了
http.NoBody或空io.NopCloser(nil),clone 后 body 仍是nil,需显式设置req.Body = http.NoBody - 某些 SDK(如 AWS SDK)会在 header 中注入动态签名,重试时签名已失效,这种场景必须禁用重试或改用签名单次请求
- 服务端若未实现幂等(如没校验
Idempotency-Key),重试 POST 请求可能导致重复下单
重试不是万能补丁,真正难的是判断"这次该不该重",尤其是混合了网络错误、服务端错误、业务错误的场景。与其堆逻辑,不如先推动上游加幂等和更细粒度的状态码。