HTTP客户端请求失败时err为nil但状态码异常,需手动检查resp.StatusCode并校验;应显式创建自定义http.Client避免DefaultClient全局污染;错误包装要保留URL等上下文;JSON解析前须验证Content-Type;需按状态码语义区分可恢复与不可恢复错误。

HTTP客户端请求失败时,err 为 nil 但响应状态码异常怎么办
Go 的 http.Client.Do 只在网络层或协议解析出错时返回非 nil 的 err;而 HTTP 状态码如 404、500 会被正常返回,err 仍是 nil。这是最常被忽略的“假成功”场景。
- 必须手动检查
resp.StatusCode,不能只依赖err != nil - 推荐在调用
resp.Body.Close()前判断状态码,避免资源泄漏 - 对业务关键接口,建议封装统一校验逻辑,例如:
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) }
使用 http.DefaultClient 导致连接复用失效或超时失控
http.DefaultClient 是全局单例,直接修改其 Timeout 或 Transport 会影响所有包(包括第三方库),极易引发隐蔽的并发问题或超时策略冲突。
- 始终显式构造自定义
*http.Client,哪怕只是复制默认配置:client := &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 30 * time.Second, }, } - 不要复用同一个
http.Client实例处理差异极大的请求(如短查询 vs 长轮询),应按场景分组管理 - 注意
http.Transport中的MaxConnsPerHost默认为 0(不限制),高并发下可能耗尽文件描述符
错误链中丢失原始 HTTP 错误上下文
用 fmt.Errorf("failed to fetch user: %w", err) 包装错误时,若 err 来自 net/http(如 net/url.Error 或 net.OpError),原始 URL、method、host 等信息会丢失,排查困难。
- 优先使用
errors.Join(Go 1.20+)或自定义错误类型保留字段 - 简单方案:在包装时显式带上关键上下文:
return fmt.Errorf("GET %s failed: %w", req.URL.String(), err) - 对重试逻辑,避免重复追加相同前缀,可用
fmt.Errorf("%w; retrying...", err)保持链式可读性
JSON 解析错误未区分是网络问题还是数据格式问题
常见写法:json.NewDecoder(resp.Body).Decode(&v) 失败后,仅靠 err 无法判断是服务端返回了非 JSON 响应(如 HTML 错误页),还是网络中断导致读取不完整。
立即学习“go语言免费学习笔记(深入)”;
- 先检查
resp.Header.Get("Content-Type")是否含application/json,再解码 - 用
io.ReadAll拿到完整响应体,方便日志记录和二次分析:body, _ := io.ReadAll(resp.Body) if !strings.Contains(resp.Header.Get("Content-Type"), "json") { return fmt.Errorf("unexpected content-type %q, body: %s", resp.Header.Get("Content-Type"), string(body)) } - 对调试环境,可临时将
body写入log.Printf,但生产环境需控制敏感信息输出
HTTP 错误处理真正的难点不在语法,而在区分「可恢复」与「不可恢复」错误——比如 503 Service Unavailable 应该重试,而 401 Unauthorized 重试只会放大问题;这类判断必须结合状态码、响应头(如 Retry-After)、甚至业务语义,没法靠一个通用 wrapper 自动解决。









