go http客户端默认不重试502/504错误,遇到此类状态码时返回resp.statuscode=502或504且err==nil,需显式检查状态码并手动重试,否则易导致json解析失败或字段为空。

Go HTTP客户端默认不重试502/504
Go标准库的http.Client对502(Bad Gateway)和504(Gateway Timeout)完全不重试——它把它们当最终错误处理,哪怕后端只是瞬时抖动。这是最常被忽略的前提:你写的http.Get或http.Do调用,遇到负载均衡器返回502/504,立刻返回*http.Response且err == nil,但resp.StatusCode是502或504,不会自动重发请求。
常见错误现象:if err != nil检查通过,但业务逻辑直接拿resp.Body解析JSON,结果解码失败或字段为空,日志里却看不到明显错误。
- 必须显式检查
resp.StatusCode >= 500 && resp.StatusCode - 不要依赖
err != nil来判断请求失败 - 502/504是服务端错误,但责任在中间链路(LB或上游),不是你的代码bug,重试通常是合理策略
用RoundTripper包装实现带条件重试
直接改http.Client.CheckRedirect没用——它只管3xx跳转;真正要拦截并重试,得替换http.Client.Transport,用自定义RoundTripper。核心是在RoundTrip方法里捕获502/504响应,并按需重新发起原始请求。
注意:重试前必须resp.Body.Close(),否则连接不释放;重试次数建议≤3次,避免雪崩;重试间隔用指数退避(如100ms、300ms、900ms)。
立即学习“go语言免费学习笔记(深入)”;
- 别用
time.Sleep硬等,用time.AfterFunc或循环中time.Sleep加select超时控制 - 原请求的
Body如果是io.ReadCloser(比如文件上传),重试时需能重放——要么用bytes.Buffer提前缓存,要么确保Body可seek(如strings.NewReader) - GET/HEAD请求才适合无脑重试;POST/PUT建议加幂等性头(如
X-Idempotency-Key)再重试
type RetryTransport struct {
base http.RoundTripper
}
<p>func (rt <em>RetryTransport) RoundTrip(req </em>http.Request) (<em>http.Response, error) {
var resp </em>http.Response
var err error
for i := 0; i < 3; i++ {
resp, err = rt.base.RoundTrip(req)
if err != nil {
if i == 2 { return nil, err }
time.Sleep(time.Millisecond <em> time.Duration(100</em>int(math.Pow(3, float64(i))))))
continue
}
if resp.StatusCode == 502 || resp.StatusCode == 504 {
resp.Body.Close()
if i == 2 { break }
time.Sleep(time.Millisecond <em> time.Duration(100</em>int(math.Pow(3, float64(i))))))
continue
}
return resp, nil
}
return resp, err
}第三方库选型:net/http vs. third-party clients
自己写RoundTripper可控但易出错(比如Body未关闭、上下文未传递)。如果项目允许引入依赖,github.com/hashicorp/go-retryablehttp是更稳的选择——它内置了502/504重试逻辑,且默认开启,还支持RetryMax、RetryWaitMin等配置。
但它有个坑:默认重试所有5xx,包括500(Internal Server Error),而500大概率是后端真实故障,不该盲目重试。所以必须覆盖CheckRetry函数:
- 用
retryablehttp.DefaultRetryPolicy作基础,再额外过滤掉500、503(Service Unavailable) - 确保
req.Context()透传到重试请求,避免父上下文取消后子请求还在跑 - 该库不兼容
http.Client.Timeout,超时必须设在retryablehttp.Client.RetryMaxTime里
client := retryablehttp.NewClient()
client.RetryMax = 2
client.RetryWaitMin = 100 * time.Millisecond
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err != nil {
return true, nil // 连接层错误重试
}
if resp.StatusCode == 502 || resp.StatusCode == 504 {
return true, nil // 明确只重试这两个
}
return false, nil // 其他状态码不重试
}负载均衡器行为差异影响重试效果
不是所有502/504都适合重试。有些LB(比如AWS ALB)返回502是因为目标组里所有实例健康检查失败,此时重试只是浪费资源;而Nginx或Traefik返回504,往往只是上游响应慢,重试成功率高。
关键看LB日志或监控:如果502/504集中出现在某台后端节点故障期间,说明LB已做剔除,重试无意义;如果分散在多个节点、伴随高延迟,重试才有价值。
- 在HTTP Header里加
X-Request-ID,方便关联LB日志和你的重试请求 - 给重试请求加
X-Retry-Count头,让后端识别并避免二次处理(比如重复扣款) - 生产环境务必记录重试次数分布:如果30%请求需要≥2次重试,说明LB或上游稳定性已出问题,不能只靠客户端补救
重试逻辑越简单越可靠,但边界情况(如Body不可重放、上下文取消、LB粘性会话)永远比文档写的多。上线前用httptest.NewUnstartedServer模拟502响应,手动验证重试是否触发、Body是否正确读取、超时是否生效——这些没法靠单元测试全覆盖。










