直接用github.com/hashicorp/go-retryablehttp需修改三处关键配置才可用:默认仅重试网络错误而忽略5xx/429/408状态码;退避策略为固定1秒易引发雪崩;未设单次请求超时且不自动透传context,须自定义CheckRetry、Backoff及每次重试新建带timeout的context。

直接用 github.com/hashicorp/go-retryablehttp 是最快落地生产级重试的方式,但默认配置会掩盖错误、忽略幂等风险、卡死在超时里——得改三处关键配置才能真正可用。
为什么不能直接 new retryablehttp.Client() 就完事
原生 retryablehttp.NewClient() 返回的 client 默认只重试网络错误(如连接超时、拒绝),完全不检查 HTTP 状态码。这意味着 500 响应会被当作成功返回,而 429(限流)或 408(请求超时)这类明确该重试的响应码反而被忽略。
- 它默认的
CheckRetry函数只判断err != nil,不看resp.StatusCode - 它默认的退避策略是固定 1s,没抖动,容易触发下游雪崩
- 它默认复用传入的
http.Client.Timeout,但没对单次请求设独立 timeout,一次慢请求会拖垮整轮重试
必须重写 CheckRetry 才能识别 5xx / 429 / 408
HTTP 层重试的核心判断不在 error,而在 response 状态码。你要显式覆盖默认逻辑:
client := retryablehttp.NewClient()
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// 有 error:网络层失败,一般可重试(但要排除 DNS、TLS 类永久错误)
if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Timeout() {
return true, nil
}
if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) {
return true, nil
}
return false, err // 其他 error(如 invalid URL)不重试
}
<pre class="brush:php;toolbar:false;">// 无 error 但状态码异常
if resp.StatusCode >= 500 && resp.StatusCode <p>}</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill6264" title="Golang Samber Hot"><img
src="https://img.php.cn/upload/skill/000/000/081/179085436125259.jpg" alt="Golang Samber Hot" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill6264" title="Golang Samber Hot" class="overflowclass">Golang Samber Hot</a>
<p class="overflowclass">在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill6264" title="Golang Samber Hot" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- 每次重试前必须
io.Copy(io.Discard, resp.Body)+resp.Body.Close(),否则 body 未读完会导致下一次RoundTrip失败 - 别漏掉
408和429,它们是服务端主动拒绝,不是客户端错,值得重试 -
4xx中只有这两个是临时性语义,其余(400/401/403/404)一律返回false
必须用 WithRetryWaitMax + WithRetryBackoff 控制退避行为
默认退避是固定 1s,高并发下所有请求会在同一时刻重试,极易打爆下游。要用指数退避 + 抖动:
client.RetryWaitMin = 100 * time.Millisecond
client.RetryWaitMax = 2 * time.Second
client.RetryMax = 3 // 注意:这是「最多重试 3 次」,含首次共 4 次
<p>// 自定义抖动退避
client.Backoff = retryablehttp.LinearJitterBackoff
// 或更推荐:
client.Backoff = func(min, max time.Duration, attemptNum int, resp <em>http.Response, err error) time.Duration {
if attemptNum == 0 {
return min
}
base := time.Duration(float64(min) </em> math.Pow(2, float64(attemptNum)))
jitter := time.Duration(rand.Int63n(int64(max - base)))
if base+jitter > max {
return max
}
return base + jitter
}</p>
-
RetryMax = 3表示「最多重试 3 次」,不是「总共执行 3 次」——这点和retry-go的Attempts(3)含义不同,别混淆 - 一定要设
RetryWaitMax,否则退避时间可能无限增长,超出你总的 context deadline - 每次重试都应新建
context.WithTimeout,不能复用外层 ctx;否则某次请求卡住,后续重试全被 cancel
别忘了业务层 request.Body 可重放问题
retryablehttp.Client 会多次调用 RoundTrip,如果原始 *http.Request 的 Body 是 io.Reader(比如 strings.NewReader),第二次重试时会因 body 已被读空而失败。
- 用
bytes.NewBuffer替代strings.NewReader构造 body - 或者提前把 body 内容读出存为
[]byte,每次重试时重新生成bytes.NewReader(bodyBytes) - 若 body 来自文件或流,必须确保实现
io.Seeker,否则无法重放 - POST/PUT 请求尤其要注意——非幂等操作重复提交,服务端没做
Idempotency-Key就等于制造脏数据
最易被忽略的一点:这个库不会自动帮你透传 context 到每次重试的底层 http.Client.Do 调用里。如果你在外部用了 ctx, cancel := context.WithTimeout(...),必须手动把 ctx 注入到每个 req = req.Clone(ctx),否则重试过程对 cancel 不敏感,goroutine 会泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










