重试逻辑不能直接塞进 http.client 里,需手动封装指数退避:设置 timeout、仅对连接错误和5xx状态码重试,读完响应体再判断,加入随机抖动防雪崩。

重试逻辑不能直接塞进 http.Client 里
Go 标准库的 http.Client 本身不提供重试机制,调用 client.Do(req) 失败就返回错误,不会自动重试。想加重试,得自己封装一层逻辑,或借助第三方库(如 github.com/hashicorp/go-retryablehttp),但多数场景下手写更可控、依赖更轻。
用 net/http + time.Retry 模拟指数退避重试
手动实现时,重点不是“重试几次”,而是“什么时候重试”——直接循环调用 client.Do 容易打爆服务或触发限流。推荐用指数退避(exponential backoff):每次失败后等待时间翻倍,并加入随机抖动(jitter)防雪崩。
-
http.Client必须设置Timeout,否则单次请求卡住会导致整个重试流程阻塞 - 只对特定错误重试:比如
net.OpError(连接拒绝/超时)、url.Error中的timeout或connection refused;不要重试 400、401、404 等客户端错误 - 5xx 响应体需显式检查:HTTP 状态码 500、502、503、504 可重试,但要先读完响应 Body 防止连接复用异常
func doWithRetry(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
var resp *http.Response
var err error
baseDelay := time.Second
for i := 0; i <= maxRetries; i++ {
resp, err = client.Do(req)
if err == nil && resp.StatusCode >= 500 && resp.StatusCode <= 599 {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if i == maxRetries {
return resp, fmt.Errorf("server error after %d retries: %d", maxRetries, resp.StatusCode)
}
delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(i))) + time.Duration(rand.Int63n(int64(time.Second)))
time.Sleep(delay)
continue
}
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && (netErr.Timeout() || netErr.Temporary()) {
if i == maxRetries {
return nil, err
}
delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(i))) + time.Duration(rand.Int63n(int64(time.Second)))
time.Sleep(delay)
continue
}
}
break
}
return resp, err
}
http.Transport 的 MaxIdleConnsPerHost 和重试强相关
重试过程中频繁新建连接,若 http.Transport 配置不当,会快速耗尽文件描述符或触发 “too many open files” 错误。关键参数必须显式调优:
-
MaxIdleConnsPerHost建议设为 100+(默认是 2),尤其在高并发重试场景下 -
IdleConnTimeout设为 30s 左右,避免空闲连接长期占用 -
TLSHandshakeTimeout和ResponseHeaderTimeout必须小于Client.Timeout,否则重试判断会失准
别忽略请求体(req.Body)的可重放性
HTTP 请求体(比如 strings.NewReader("json=..."))默认只能读一次。重试时若直接复用原 *http.Request,第二次 client.Do 会因 Body 已关闭或 EOF 而失败。
立即学习“go语言免费学习笔记(深入)”;
- 简单场景:用
bytes.NewBuffer替代strings.NewReader,它支持多次读取 - 通用方案:把原始 payload 存为字节切片,在每次重试前重建
req.Body = io.NopCloser(bytes.NewReader(payload)) - JSON 场景可提前序列化:避免每次重试都调用
json.Marshal,也方便日志和调试
重试不是加个 for 循环就完事——连接复用、请求体重放、错误分类、退避节奏,每一步漏掉都可能让重试变成故障放大器。










