Go标准库不提供HTTP重试机制,必须手动封装;推荐用github.com/cenkalti/backoff/v4实现可控重试,需注意错误分类、req.Clone、上下文取消及body可重放性。

重试逻辑必须自己写,标准库不提供
Go 的 net/http 和 context 包里没有 Retry、DoWithRetry 或任何自动重试函数。你看到的“重试生效”,大概率是服务端 302 重定向、代理层行为,或是别人已经封装好的工具。别信“设了 Client.Timeout 就会重试”这种说法——它只控制单次请求超时,失败后直接返回错误,绝不会多发一次。
用 backoff.Retry 封装最省心,但得配对用对参数
社区事实标准是 github.com/cenkalti/backoff/v4,它把退避策略、上下文取消、最大重试次数都收口了。手写 for+time.Sleep 容易漏掉 jitter、忽略 ctx.Done()、共享退避状态错乱,还可能阻塞 goroutine。
-
backoff.NewExponentialBackOff()默认InitialInterval=100ms、MaxElapsedTime=10s,但没开 jitter,也没显式设最大重试次数,线上慎用 - 必须包一层
backoff.WithJitter(bo),否则多个请求在相同时间点重试,容易触发下游雪崩 - 必须用
backoff.WithContext(bo, ctx),否则重试过程完全无视 cancel 或 timeout - 错误分类要前置:网络类(
net.OpError、context.DeadlineExceeded)、5xx/429 响应可重试;400/401/404 等业务错误必须用backoff.Permanent(err)拦住
req.Clone(ctx) 不是可选项,是必做项
HTTP 请求重试失败最常见的原因是 body 不可重放。req.Body 是一次性读取流,第一次 Do() 后就关闭或 EOF。下次再用原 *http.Request,会报 http: Request.Body is nil 或 http: read on closed response body。
- 每次重试前必须调
req.Clone(ctx),生成新请求实例 - 如果原始
req.Body是strings.NewReader或bytes.NewReader,克隆后可用;但os.Stdin、文件句柄、管道等不可克隆,得提前缓存成 bytes - 别试图复用
req.Headermap——虽然它能共享,但修改它会影响所有克隆请求,容易串扰
封装成 RetryClient 时,别暴露底层 http.Client
结构体封装比函数更利于配置统一和复用,但字段设计很关键。导出一个可被外部随意改的 http.Client 字段,等于把重试语义全交出去了——别人改了 Timeout 或 Transport,你的重试间隔、错误判断就全失效。
立即学习“go语言免费学习笔记(深入)”;
- 只暴露
Do(req *http.Request) (*http.Response, error)方法 - 把
MaxRetries、BackOff、ShouldRetry(错误判断函数)设为私有字段,初始化时锁定 - 不同接口需要差异化重试策略?别共用一个全局 client,按需 new
RetryClient实例 - 上传类请求(如大文件 POST)默认不该重试 500,因为服务端可能已接收部分数据;查询类可以重试,这个差异必须由调用方传参或实例化时决定


















