直接用net/http手动重试会出问题,因为http.client默认不重试,业务层for循环易漏网络底层错误(如dns失败、tls握手超时),且http.request非线程安全,复用会导致panic;指数退避硬编码耦合业务,而retryablehttp需调整retrypolicy仅重试5xx/429/408,禁用默认backoff并配置合理重试参数。

为什么直接用 net/http 做重试会出问题
因为 http.Client 默认不重试,而手动在业务层加 for 循环重试时,容易漏掉连接中断、DNS 失败、TLS 握手超时这些底层错误;更麻烦的是,http.Request 不是线程安全的,多次复用同一请求对象可能触发 invalid memory address panic。指数退避逻辑如果写在 handler 里,还会和业务逻辑耦合,不同接口要重复写 time.Sleep(1 * time.Second)、time.Sleep(2 * time.Second) 这类硬编码。
用 retryablehttp 替代原生 http.Client 的关键配置
社区成熟的 retryablehttp 库(by hashicorp)已经封装了标准退避策略,但默认行为仍需调整才能适配真实后端服务:
- 把
RetryMax设为 3~5,避免长尾请求拖慢整体响应; - 显式设置
RetryWaitMin和RetryWaitMax,例如RetryWaitMin: 100 * time.Millisecond、RetryWaitMax: 2 * time.Second,防止首次重试等待过久; - 必须覆盖
RetryPolicy,只对5xx和部分4xx(如429、408)重试,跳过400、401、403等客户端错误; - 禁用
RetryBackoff默认函数,改用retryablehttp.LinearJitterBackoff或自定义指数函数,避免多个实例在同一时刻集中重试打爆下游。
示例片段:
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 100 * time.Millisecond
client.RetryWaitMax = 2 * time.Second
client.RetryPolicy = func(ctx context.Context, resp *http.Response, err error) bool {
if err != nil {
return true // 底层网络错误一律重试
}
return resp.StatusCode >= 500 || resp.StatusCode == 429 || resp.StatusCode == 408
}
client.Backoff = retryablehttp.LinearJitterBackoff
如何让重试逻辑穿透中间件和拦截器
如果你用了 gorilla/mux 或 chi,又或者封装了鉴权、trace、metric 中间件,重试必须发生在最外层 HTTP 客户端,而不是某一层 middleware 里——否则每次重试都会重新执行鉴权或生成新 traceID,导致日志混乱、权限校验失败。
- 所有业务请求统一走一个封装好的
DoWithRetry()函数,它接收*http.Request并返回*http.Response; - 不要在 middleware 中调用
next.ServeHTTP()后检查状态码再重试,那只是重试 handler 执行,不是重试 HTTP 请求; - 若需携带 traceID 或 auth token,应在构造
*http.Request时注入,而不是靠中间件动态加 header——重试时 request 是全新构造的。
重试时怎么避免 body 被读空或重复提交
http.Request.Body 是单次读取流,第一次 io.Copy 或 json.NewDecoder 之后就 EOF 了,重试时若没重置 body,会发出去空请求体,后端报 400 Bad Request。
- 对 JSON 请求,用
bytes.NewReader(jsonBytes)构造 body,每次重试都 new 一个; - 对表单或文件上传,提前用
io.ReadAll(req.Body)缓存原始 bytes,然后在每次重试前用bytes.NewReader(cachedBytes)创建新 body; - 绝对不要复用
req.Body,也不要依赖req.GetBody——它只在ContentLength > 0且 body 可 seek 时才有效,而很多代理或网关会丢掉Content-Length头。
一个易忽略的点:如果上游传入的是 io.ReadCloser(比如从另一个 HTTP 请求 copy 来的),必须确保它支持多次 Read,否则重试会静默失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











