Go 的 http.Get 和 client.Do 不会因 404/500 返回 error,err == nil 仅表示网络层成功;业务是否成功必须显式检查 resp.StatusCode,推荐用 resp.StatusCode >= 200 && resp.StatusCode < 300 判断。

Go 的 http.Get 和 client.Do 不会因 404/500 返回 error
这是最常被误判的点:只要网络通畅、服务端返回了 HTTP 响应(哪怕内容是 HTML 错误页或 JSON 错误体),err 就是 nil。你拿到的 *http.Response 是有效的,resp.StatusCode 才是唯一可信的业务成功信号。
常见错误现象:代码只检查 if err != nil 就认为请求“成功”,接着直接 io.ReadAll(resp.Body) 解析 JSON,结果把 {"errcode":401,"errmsg":"token expired"} 当成正常数据解,panic 或逻辑错乱。
- 必须显式判断
resp.StatusCode,推荐用resp.StatusCode >= 200 && resp.StatusCode ,比只比 <code>== http.StatusOK更健壮(兼容 201、204 等) - 如果状态码不在预期范围,不要跳过
resp.Body—— 它可能含关键错误信息,但需先读再关,否则连接复用会出问题 -
defer resp.Body.Close()要写在检查状态码之前,避免 panic 时漏关
非 200 响应体怎么安全读取?io.ReadAll 前必须限流
服务端对 4xx/5xx 响应仍可能返回大体积 body(比如堆栈日志、HTML 错误页、冗长 debug 信息)。直接 io.ReadAll(resp.Body) 可能 OOM,尤其在高并发场景下。
正确做法不是“不读”,而是“可控地读”:
立即学习“go语言免费学习笔记(深入)”;
- 用
http.MaxBytesReader包一层resp.Body,例如限制最多读 1MB:io.ReadAll(http.MaxBytesReader(nil, resp.Body, 1 - 若只需错误提示,可用
bufio.NewReader(resp.Body).ReadString('\n')读首行,或io.LimitReader(resp.Body, 4096)截断 - 读完后仍要
resp.Body.Close()—— 即使读到 EOF 或 error,也要关,否则底层连接无法复用
如何统一拦截并分类处理不同状态码?封装 checkResponse 函数
重复写 switch resp.StatusCode 很容易漏 case 或处理不一致。建议抽成可复用函数,把“感知”和“响应”解耦:
- 函数签名类似:
func checkResponse(resp *http.Response) error - 内部按范围分类:2xx 返回 nil;4xx 返回带
ClientError类型的 error;5xx 返回ServerError并可触发重试逻辑 - 对 401/403 这类认证相关状态码,可额外提取
resp.Header.Get("WWW-Authenticate")辅助诊断 - 别在函数里直接
log.Fatal或 panic —— 让调用方决定是重试、降级还是上报
为什么 resp.Body 在非 200 时有时读不到内容?
这不是 Go 的 bug,而是服务端行为差异:有些 API(如 RESTful 设计良好的接口)对 400/500 会返回结构化 JSON;有些 Web 服务器(如 nginx 默认配置)则返回 HTML 错误页;还有些直接返回空 body。
关键点在于:Go 客户端从不屏蔽非 200 的 Body,它始终可用 —— 读不到内容,是因为服务端根本没写。
- 用
curl -v或netcat对比请求头,确认 Go 客户端和服务端期望的 Accept 头是否一致(比如服务端只对Accept: application/json返回 JSON 错误体) - 检查服务端是否做了 “短路响应”:某些框架在 500 时跳过 middleware,导致错误体未生成
- Go 侧可加
req.Header.Set("Accept", "application/json")显式声明期望格式,提高错误体可读性


















