Go标准库http.Client不自带重试机制,需手动封装;推荐用backoff.Retry实现可控重试,注意错误分类、body可重放性、请求头一致性及上下文取消。

Go语言用http.Client自带重试机制?别信
Go标准库的http.Client本身**不自动重试任何请求**,哪怕遇到net.ErrTimeout、net/http: request canceled或5xx响应。这是设计选择,不是bug。很多人以为设置Timeout或Transport就能触发重试,结果线上服务出问题才意识到根本没重试。
真正可控的做法是自己封装重试逻辑,而不是依赖Client“隐式行为”。
-
http.Client的CheckRedirect只管重定向(301/302),和网络失败重试无关 -
Transport里的MaxIdleConnsPerHost等参数影响连接复用,但不会让失败请求自动再发一次 - 直接对
resp, err := client.Do(req)循环调用,必须手动处理req的body可重放性——比如strings.NewReader可以,但os.Stdin不行
用backoff.Retry做带退避的HTTP重试最省心
社区最常用的是github.com/cenkalti/backoff/v4,它把退避策略(指数增长、抖动)、停止条件、错误过滤都封装好了,比手写for-loop+time.Sleep可靠得多。
关键不是“能不能重试”,而是“重试哪些错误”和“重到第几次停”。比如400、401不该重试,而context.DeadlineExceeded、net.OpError、http.ErrUseLastResponse才值得重试。
- 用
backoff.WithMaxRetries明确上限,比如3次——太多会放大下游压力 - 用
backoff.WithContext支持提前取消,避免goroutine泄漏 - 每次重试前要
req.Clone(req.Context()),否则body可能已关闭或不可读 - 示例核心片段:
err := backoff.Retry(func() error { resp, err := client.Do(req.Clone(ctx)) if err != nil { return backoff.Permanent(err) // 永久错误,不重试 } if resp.StatusCode >= 500 { return fmt.Errorf("server error: %d", resp.StatusCode) } return nil }, b)
自定义RoundTripper拦截重试?风险高,慎用
有人想在http.Transport层统一加重试,比如包装RoundTrip方法。这看起来“一劳永逸”,但实际踩坑密集:
- 无法区分业务错误(如404)和传输错误(如连接拒绝),容易把不该重试的也重试了
- response body未读完就重试,会导致
http: read on closed response body - 中间件式重试会干扰trace、metrics等可观测性逻辑,debug时难定位是哪次请求成功
- 如果底层用了
h2c或http/3,某些错误类型在不同协议下表现不一致,重试逻辑可能失效
重试时别忘了Content-Length和Transfer-Encoding
POST/PUT带body的请求重试,最容易忽略的是HTTP头和body状态的一致性。标准库在第一次Do后可能已经修改了req.Header,比如自动加Content-Length;如果直接重用req,第二次发送时可能头和body长度对不上,导致服务端解析失败或返回400。
- 安全做法:每次重试都用
http.NewRequestWithContext新建请求,或至少req = req.Clone(ctx) - 如果body是
io.Reader且不可seek(比如os.File),必须确保它支持多次读——否则得先io.ReadAll进内存再用bytes.NewReader - 显式设置
req.ContentLength比依赖自动推导更可控,尤其当body是自定义io.ReadSeeker时
重试不是加个for循环就完事,最麻烦的是body可重放性、错误分类粒度、以及和上下文取消的配合——这些地方一错,轻则重试无效,重则引发雪崩。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











