http客户端超时必须配置http.client.timeout而非仅http.transport;重试需手动实现,仅对网络层错误和5xx重试,避免盲目重试4xx或解析错误。

HTTP客户端超时设置必须用http.Client,不能只设http.Transport
很多人误以为给http.Transport设置了Timeout或IdleConnTimeout就等于整个请求超时,其实不是。Go 的 http.Client 超时是分层的:Timeout 是端到端总时限(DNS + 连接 + TLS + 发送 + 接收),而 Transport 里的各项超时只控制底层连接行为。
正确做法是直接配置 http.Client 的 Timeout 字段:
client := &http.Client{
Timeout: 10 * time.Second,
}
-
Timeout会覆盖Transport中所有子超时(如DialContextTimeout),只要它非零 - 如果需要更精细控制(比如连接最多 3 秒、读响应最多 7 秒),就得自定义
Transport并配DialContext和ResponseHeaderTimeout - 注意:一旦设置了
Timeout,context.WithTimeout在Do里就是冗余的,除非你要做 cancel 传播
重试逻辑必须自己实现,http.Client 不支持内置重试
Go 标准库的 http.Client 明确不提供重试机制——它连“是否因网络失败而重试”都不判断,所有错误都原样返回。常见错误如 net/http: request canceled、net/url: invalid URL、context deadline exceeded、EOF 都需要你手动分类处理。
基础重试模板建议用 for 循环 + time.Sleep,配合指数退避:
立即学习“go语言免费学习笔记(深入)”;
for i := 0; i < 3; i++ {
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if i == 2 {
return nil, err
}
time.Sleep(time.Second * time.Duration(1 << i)) // 1s, 2s, 4s
}
- 只对临时性错误重试:网络断开、5xx、连接拒绝;跳过 4xx(如 400/401/404)和解析类错误(
url.Error、json.SyntaxError) - 每次重试前要
req = req.Clone(ctx),否则 body 可能已被读取或关闭 - 如果用了自定义
context(比如带 timeout 的),记得在每次重试时新建一个,避免上一次 cancel 泄露影响下一次
结合 context 控制超时与取消,但别和 Client.Timeout 冲突
当业务需要动态控制单次请求生命周期(比如用户主动取消、上游服务限流),应该用 context.WithTimeout 或 context.WithCancel 包裹 req,而不是依赖 Client.Timeout。两者可以共存,但优先级不同。
推荐组合方式:
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req) // client.Timeout 设为 0,完全交由 ctx 控制
- 把
client.Timeout设为 0,让 context 成为唯一超时源,逻辑更清晰 - 若同时设了
client.Timeout和req.Context(),以先触发者为准,但错误类型不同(前者是context.DeadlineExceeded,后者可能是net/http: request canceled) - 注意:中间件或 SDK(如
github.com/go-resty/resty/v2)可能已封装 context,此时需确认它是否透传 cancel
生产环境务必区分错误类型,别盲目重试 4xx 或 body 解析失败
重试不是万能解药。很多线上问题源于对错误码无差别重试,比如反复 POST 同一个非法 JSON,结果触发风控或写入重复数据。
典型需跳过重试的场景:
resp.StatusCode >= 400 && resp.StatusCode :客户端错误,重试无效-
err != nil && strings.Contains(err.Error(), "invalid character"):JSON 解析失败,说明响应格式不对,不是网络问题 -
url.Error中Err是*net.OpError且Op == "dial":可重试;但若是"read"且Err == io.EOF,大概率是服务端提前关连接,需看 StatusCode 再决定
真正该重试的,只有明确属于传输层中断的错误:连接超时、TLS 握手失败、写入被拒、读响应中途断连(且无 status code)。
这些边界情况容易被忽略——尤其是当服务端返回 200 但 body 截断时,json.Unmarshal 报错,你却还在 retry。










