Go中http.Client.Do()的error来源多样:网络问题、客户端配置错误、URL解析失败或重定向循环;需先检查err再处理resp,区分错误类型并关闭resp.Body。

HTTP请求失败时,err到底从哪来?
Go 的 http.Client.Do() 会返回两个值:*http.Response 和 error。这个 error 不只来自网络连通性问题(比如 DNS 失败、连接超时),还可能来自客户端配置错误(如 http.DefaultClient 被意外置为 nil)、URL 解析失败(url.Parse() 报错但你没检查)、甚至重定向循环被主动终止。别默认 err != nil 就等于“服务不可达”——它可能根本没发出去。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 始终先检查
err,再检查resp.StatusCode;err非空时,resp可能为nil,直接解引用会 panic - 区分错误类型:用
errors.Is(err, context.DeadlineExceeded)判断超时,用url.Error类型断言提取底层错误(如net.OpError) - 不要忽略
resp.Body:即使出错,只要resp != nil,也要调用resp.Body.Close(),否则可能泄漏 TCP 连接
用 context.WithTimeout 控制请求生命周期
Go 的 HTTP 客户端本身不提供全局超时,必须靠 context 注入。直接设置 http.Client.Timeout 只控制整个请求(DNS + 连接 + 写请求 + 读响应),但无法区分“卡在 TLS 握手”还是“后端响应慢”。用 context 可以更精细地控制,比如给重试留出余量。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每次请求都新建带超时的
context,例如ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - 把
ctx传给req.WithContext(ctx),而不是直接改http.Client.Timeout - 注意:如果重试逻辑在外层,每次重试都应创建新
ctx,否则前一次超时会污染下一次
简单重试不是加个 for 循环就完事
盲目重试会放大问题:对 400 Bad Request 重试没意义;对 503 Service Unavailable 重试可能压垮下游;并发请求下未限流的重试可能触发熔断。Go 标准库不提供重试机制,得自己控制节奏和条件。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 只对可重试错误重试:网络层错误(
net.OpError)、5xx 状态码、context.DeadlineExceeded(但排除context.Canceled) - 用指数退避:第一次等 100ms,第二次 200ms,第三次 400ms……避免雪崩,可用
time.Sleep(time.Duration(math.Pow(2, float64(attempt))) * time.Millisecond) - 限制最大重试次数(通常 3 次足够),并在日志中记录重试原因和最终结果,方便排查是偶发故障还是服务持续异常
自定义 http.RoundTripper 实现透明重试
如果多个地方都要重试,重复写 for 循环容易出错。可以封装一个带重试能力的 RoundTripper,让 http.Client 自动处理。但要注意:标准 http.Transport 会复用连接,而重试时需确保请求体(尤其是 io.ReadCloser)可重放——否则第二次读就是空的。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 若请求体是字符串或字节切片,用
bytes.NewReader()构造可重放的Body;避免直接传os.Stdin或文件句柄 - 重试逻辑里要克隆
*http.Request:调用req.Clone(req.Context()),并重设Body - 不要在
RoundTripper.RoundTrip中直接修改原始req,否则影响其他中间件或日志记录
重试逻辑最易被忽略的点是请求体的可重放性——很多 bug 表现为“第一次成功、重试全 400”,根源就在 Body 被读过一次后变为空。











