HTTP请求需同时检查err和resp.StatusCode,正确关闭resp.Body,复用配置合理的http.Client,分层处理网络、状态码、JSON解析及业务错误。

HTTP请求失败时resp为nil,但err不总是可靠
Go的http.DefaultClient.Do()在底层连接失败(如DNS解析失败、网络不可达)时会返回非nil的err且resp == nil;但若服务器返回4xx/5xx状态码,err仍为nil,resp存在但resp.StatusCode异常。很多人只检查err就认为请求“成功”,结果把404当正常响应处理。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须同时检查
err和resp.StatusCode,尤其对非2xx响应要显式判断 - 用
http.Client时建议设置Timeout,避免Do()无限阻塞(默认无超时) - 不要依赖
errors.Is(err, context.DeadlineExceeded)来区分超时——某些底层错误(如TLS握手失败)可能不包裹该错误,应统一用net.Error.Timeout()判断
如何安全读取resp.Body并避免panic
resp.Body是io.ReadCloser,必须关闭;但一旦err != nil且resp == nil,直接访问resp.Body会panic。即使resp != nil,若未读完或未关闭,可能造成连接复用失效或内存泄漏。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
defer关闭前先判空:if resp != nil && resp.Body != nil { defer resp.Body.Close() } - 读取响应体推荐用
io.ReadAll(resp.Body),而非resp.Body.Read()手动循环——后者易漏错误、难控制长度 - 对大响应体,用
io.Copy流式处理,并配合http.MaxBytesReader限制最大读取字节数,防OOM
http.Client复用不当引发连接泄漏或超时累积
每次新建&http.Client{}而不设Transport,会使用默认http.DefaultTransport,其MaxIdleConns等参数可能不适用高并发场景;更隐蔽的问题是:若忘记调用resp.Body.Close(),底层连接无法归还到idle池,导致后续请求排队甚至触发context deadline exceeded。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 全局复用一个
*http.Client实例,按需配置Transport,例如:client := &http.Client{Timeout: 10 * time.Second, Transport: &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 100}} - 避免在短生命周期函数里新建
Client,尤其在HTTP handler中 - 用
curl -v或netstat观察ESTABLISHED连接数突增,往往是Body未关闭的信号
JSON反序列化失败不是HTTP错误,但常被混为一谈
json.Unmarshal()失败返回err != nil,但这与HTTP传输无关。如果服务端返回了200但JSON格式错误(如字段类型错、缺少必需字段),仅靠检查resp.StatusCode无法捕获。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 定义结构体时善用
json:",omitempty"和json:"field_name,omitempty"减少反序列化失败概率 - 对关键字段做运行时校验,比如
if user.ID == 0 { return errors.New("missing user ID") } - 不要把
json.Unmarshal错误日志写成“HTTP请求失败”——它掩盖了真实问题边界
HTTP错误管理真正的难点不在语法,而在分清错误来源层级:网络层、协议层(status code)、表示层(JSON/XML解析)、业务逻辑层。每一层都该有独立的错误分类和处理策略,混在一起只会让重试、监控、告警全部失焦。









