go http客户端必须显式设置超时,http.defaultclient无默认超时易卡死;需用带timeout的自定义client或http.transport细粒度控制,并通过req.withcontext传递context;错误需区分网络/业务错误,resp.body须defer关闭;重试应指数退避且仅针对可重试错误。

Go HTTP客户端默认不超时,http.DefaultClient 会卡死
Go 的 http.DefaultClient 没有设置任何超时,遇到网络抖动、服务无响应或 DNS 卡住时,Do() 会一直阻塞,直到 TCP 层最终断开(可能长达几分钟)。这不是“慢”,是彻底不可控的挂起。
必须显式构造带超时的 http.Client,而不是依赖默认值:
client := &http.Client{
Timeout: 10 * time.Second,
}注意:Timeout 是整个请求的总时限(DNS + 连接 + 写请求 + 读响应),不是某一段的单独限制。如果需要更细粒度控制(比如连接最多 3 秒、读响应最多 7 秒),得用 http.Transport 配置:
-
DialContext控制 DNS + 建连时间 -
ResponseHeaderTimeout控制从建连完成到收到响应头的时间 -
ReadTimeout和WriteTimeout在 Go 1.12+ 已被弃用,应统一用Timeout或上述上下文级参数
context.WithTimeout 要和 req.WithContext 配合用才生效
只给 http.Client 设 Timeout,或只调 context.WithTimeout 但没传进请求,都无效。Go 的 HTTP 客户端靠 context 传递取消信号,必须把 context 绑定到 *http.Request 上。
立即学习“go语言免费学习笔记(深入)”;
常见错误写法:
// ❌ 错:context 创建了,但没塞进 request ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) resp, err := client.Do(req) // 超时完全不触发
正确写法:
// ✅ 对:用 req.WithContext() 把 context 注入请求 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req = req.WithContext(ctx) resp, err := client.Do(req)
特别注意:如果用了 http.NewRequest,它返回的 *http.Request 默认绑定 context.Background(),必须手动换掉;如果用了 http.NewRequestWithContext,就一步到位。
错误类型判断不能只看 err == nil,要区分网络错误和业务错误
HTTP 请求失败后,err 可能是非空,但 resp 也可能非空(比如服务端返回了 500 状态码但 body 不完整)。反过来,err 为 nil 也不代表成功——resp.StatusCode 可能是 400/502/429 等。
典型误判场景:
- 把
net/http: request canceled当成服务端错误(其实是 context 超时或主动 cancel) - 把
net/url.ParseError或net/http: invalid header field name当成远程服务问题(其实是本地构造请求出错) - 忽略
resp.Body没 close,导致连接复用失效、fd 耗尽
建议统一检查顺序:
if err != nil {
// 先看是不是 context 相关错误
if errors.Is(err, context.DeadlineExceeded) {
log.Println("request timeout")
} else if errors.Is(err, context.Canceled) {
log.Println("request canceled")
} else {
log.Printf("network error: %v", err)
}
return
}
defer resp.Body.Close() // 即使 status 不是 2xx 也要关
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("bad status code: %d", resp.StatusCode)
return
}重试逻辑别直接套 for 循环,要用指数退避 + 判断可重试性
简单 for 循环重试(比如“失败就再试两次”)在真实网络中容易雪崩:上游服务已过载,你还密集重试,反而加剧问题。而且不是所有错误都适合重试——400 Bad Request 或 401 Unauthorized 重试毫无意义。
只对以下情况考虑重试:
-
context.DeadlineExceeded、net.OpError(连接拒绝、超时、i/o timeout) - 5xx 响应(尤其是 502/503/504)
- 部分 429(需结合
Retry-Afterheader)
重试间隔必须指数增长,例如 100ms → 200ms → 400ms,并加随机抖动避免同步冲击。标准库不提供重试,推荐用 github.com/hashicorp/go-retryablehttp 或手写带 jitter 的循环,别自己裸写 sleep。
还有一点常被忽略:重试时要新建 *http.Request,因为旧 request 的 body 可能已被读取或关闭,再次提交会 panic 或发送空体。
超时和重试叠加时,外层 context 应覆盖全部重试周期,否则可能某次重试刚发起就被父 context 取消。










