Go HTTP错误处理不能仅依赖err != nil,因4xx/5xx响应时err为nil而resp.StatusCode才反映真实状态;需显式检查StatusCode并分2xx/4xx/5xx三类处理,确保状态码语义准确匹配错误来源。

Go 语言处理 HTTP 请求错误码,不能只靠 err != nil 判断是否“失败”——400、404、500 这些状态码返回时 err 依然是 nil,必须显式检查 resp.StatusCode 并分类响应。
为什么只检查 err 会漏掉 4xx/5xx 错误
Go 的 http.Client.Do() 在网络层出问题(如 DNS 失败、连接超时)时返回非空 err 且 resp == nil;但只要 TCP 连通、服务器发回了响应(哪怕内容是 {"error":"not found"}),err 就是 nil,resp.StatusCode 才是唯一可信依据。
常见误写:
resp, err := client.Do(req)
if err != nil {
// 这里只捕获了网络失败,404 会直接进下面逻辑
log.Println("network error:", err)
return
}
// 此处 resp.StatusCode 可能是 404 或 500,但没人检查
- 用
http.Get()或http.Post()同样适用该规则,它们底层都调用Do() - 不检查
StatusCode就直接json.Unmarshal(resp.Body),可能把错误 JSON 当作正常数据解析,导致字段缺失 panic - 调试时用
curl -v看到HTTP/1.1 404 Not Found,但 Go 程序却打印 “success”,就是这个原因
如何正确分类处理 2xx / 4xx / 5xx 响应
业务逻辑需要区分三类响应:成功(2xx)、客户端错(4xx)、服务端错(5xx)。不要统一当错误丢弃,尤其 4xx 往往含有效提示信息。
推荐做法:
- 对
resp.StatusCode >= 200 && resp.StatusCode < 300视为成功路径,继续读取 body - 对
400 <= resp.StatusCode && resp.StatusCode < 500,通常表示参数错、权限不足、资源不存在,可提取响应体中的message字段返回给上游或记录 - 对
500 <= resp.StatusCode,属于下游服务异常,应记日志、考虑降级或重试,避免把原始错误透传给前端 - 用
http.StatusText(code)获取标准描述(如http.StatusText(404)→"Not Found"),比硬编码字符串更安全
怎么避免 WriteHeader 写多次或顺序错
w.WriteHeader() 必须在任何 w.Write() 或 json.NewEncoder(w).Encode() 之前调用,且只能调用一次。否则 Go 会输出 superfluous response.WriteHeader 警告,且实际生效的是第一次写入的状态码。
典型陷阱:
- 在 handler 中多个
if err != nil分支各自调用http.Error(w, ...),但没加return,后续代码又调用w.WriteHeader() - 用
defer w.WriteHeader(http.StatusOK)—— defer 在函数结束才执行,此时可能 body 已经写过,状态码被忽略或报错 - 依赖默认行为:不调用
WriteHeader()时 Go 自动设 200,但一旦你手动写了 body,再调WriteHeader(400)就无效了
安全写法是:先决定状态码 → 立即 w.WriteHeader(code) → 再写 body。
自定义错误类型让状态码携带上下文
原生 error 接口无法附带 HTTP 状态码,导致中间件或统一错误处理器无法自动映射。定义一个可识别的错误类型更可控。
例如:
type HTTPError struct {
Code int
Msg string
Err error // 底层原始错误,用于日志
}
func (e *HTTPError) Error() string { return e.Msg }
在 handler 中主动构造:
- 参数校验失败:
return &HTTPError{Code: http.StatusBadRequest, Msg: "missing user_id"} - 调第三方 API 返回 404:
return &HTTPError{Code: http.StatusNotFound, Msg: "user not found in auth service"} - 数据库连不上:
return &HTTPError{Code: http.StatusInternalServerError, Err: dbErr}
配合中间件,在 ServeHTTP 中用 errors.As(err, &e) 提取 e.Code 并设置响应头,body 统一序列化为 {"code":400,"message":"..."} 格式。
真正容易被忽略的是:**状态码语义必须与错误来源严格匹配**。比如第三方服务返回 503,你不该转成 500 再抛给前端——这会掩盖真实故障点;同样,401 和 403 不该全塞进 400 里糊弄过去。每个状态码背后都是客户端可操作的信号,乱用等于关掉了调试通道。


















