HTTP状态码应由handler显式控制,中间件无法在响应体写出后修改;必须在Write前设置,推荐使用net/http中的http.StatusOK等常量,避免硬编码数字。

HTTP状态码该由谁控制:handler 还是中间件
Go 的 http.ResponseWriter 状态码默认是 200,一旦写入响应体(比如调用 Write 或 WriteString),状态码就无法再修改。这意味着你不能在 handler 执行完之后、响应已写出的情况下靠中间件“补救”状态码。
常见错误是:中间件想统一处理错误并设成 500,但 handler 已经写了响应,此时调用 w.WriteHeader(500) 完全无效,客户端仍收到 200。
- 状态码必须在响应体写入前由 handler 显式设置,或由封装的响应工具提前控制
- 中间件只适合做日志、鉴权、超时等不依赖响应体的操作;若需干预状态码,只能在 handler 返回 error 后、写响应前拦截(例如用自定义
ResponseWriter包装) - 不要依赖
defer在 handler 结尾统一设状态码——万一 panic 或提前 return,逻辑就漏了
2xx/4xx/5xx 的典型使用场景和边界
Go 没有强制规范,但 HTTP 语义必须对齐 RFC 7231。实际开发中容易混淆的是 400 和 422、401 和 403、500 和 502。
-
400 Bad Request:请求语法错误,比如 JSON 解析失败、URL 参数缺失必填字段(id=但没值) -
422 Unprocessable Entity:语法正确但语义非法,比如 JSON 字段类型错("age": "abc")、业务规则校验失败(余额不足) -
401 Unauthorized:缺 token 或 token 过期,需要重新认证;403 Forbidden:token 有效但权限不足(如普通用户访问管理员接口) -
500 Internal Server Error:服务端 panic、DB 连接崩、空指针——未知错误兜底;502 Bad Gateway仅用于反向代理场景(如 gin 前面挂 nginx 转发到 Go 服务,Go 挂了才返回 502)
如何避免硬编码状态码数字
直接写 w.WriteHeader(404) 可读性差,也容易打错。标准库 net/http 提供了全部常量,应该直接用。
立即学习“go语言免费学习笔记(深入)”;
import "net/http"
func handler(w http.ResponseWriter, r *http.Request) {
if id := r.URL.Query().Get("id"); id == "" {
w.WriteHeader(http.StatusNotFound) // ✅
// w.WriteHeader(404) // ❌ 不推荐
w.Write([]byte(`{"error": "id required"}`))
return
}
}
- 所有标准状态码都以
http.StatusXXX形式存在,IDE 可自动补全,也方便 grep 统计 - 不要自定义非标准码(如 499、599),Nginx、CDN、监控系统可能无法识别
- 如果必须扩展语义(如 “资源被软删除”),优先用标准码 + 响应体字段说明,而不是造新状态码
JSON API 中状态码与 body 的一致性处理
很多团队习惯所有接口都返回 {"code": 0, "msg": "", "data": {}} 结构,然后永远用 200。这破坏了 HTTP 语义,让 Nginx 日志、Prometheus 监控、浏览器 DevTools 失去状态判断能力。
- 成功用
http.StatusOK,错误按语义用对应 4xx/5xx,body 里可以保留code字段作业务码(如"code": 1002表示“库存不足”),但 HTTP 状态码必须真实反映交互结果 - 不要在 200 下塞 “error” 字段来模拟失败——客户端要自己解析 body 才知道出错了,curl -I 或 curl -s 都看不到问题
-
前端 fetch 默认只对 4xx/5xx 抛 reject,如果你全用 200,就得每处手动检查
res.code !== 0,极易遗漏
状态码不是装饰,是协议契约。错用一次,排查链路就多绕三步。










