核心是writeheader必须在任何write调用前执行,否则无效;一旦w.write()隐式发送200,后续writeheader调用将被忽略,客户端始终收到200。

Go 服务端返回状态码,核心就一条:WriteHeader 必须在任何 Write 调用之前执行,否则无效。这不是建议,是 net/http 的底层契约——一旦响应头被隐式写出(比如第一次 w.Write() 时自动补 200),再调 w.WriteHeader() 就完全没反应,客户端收到的永远是 200。
WriteHeader 被忽略的典型场景
很多问题其实不是写错了,而是被中间件、日志或 panic 恢复逻辑悄悄“抢跑”了响应头。
- 中间件里调了
w.Write()或http.Error(),后续 handler 再调w.WriteHeader(404)→ 无效 - handler 里先
w.Write([]byte("ok")),再w.WriteHeader(500)→ 状态码仍是 200 - recover 中间件没在最外层包裹 handler,panic 后
http.Error()被上层框架吞掉,最终返回空白 200 页面 - 用了
json.NewEncoder(w).Encode()却没提前设状态码 → 默认 200,即使业务逻辑想返回 400
返回 JSON 错误时别混用 http.Error 和 json.Marshal
http.Error() 强制设 4xx/5xx 状态码,且固定 Content-Type 为 text/plain,和 JSON 响应冲突。想返回带状态码的 JSON,必须手动控制三步:
- 先调
w.Header().Set("Content-Type", "application/json; charset=utf-8") - 再调
w.WriteHeader(http.StatusBadRequest)(不能晚于写 body) - 最后用
json.NewEncoder(w).Encode(errResp),别用json.Marshal()+w.Write(),避免中文乱码或空格截断
结构体字段必须首字母大写,且用 json:"code" 这类 tag 显式声明,否则前端收不到 code 字段。
204 No Content 的正确姿势
删资源、改状态成功但无需返回数据时,用 http.StatusNoContent 最干净。它要求响应体为空,所以不能跟 w.Write() 配合用。
- ✅ 正确:
w.WriteHeader(http.StatusNoContent),不调w.Write() - ❌ 错误:
w.WriteHeader(http.StatusNoContent); w.Write(nil)或w.Write([]byte{})—— Go 会把它当 200 处理 - 注意:Gin 的
c.NoContent(204)是安全封装,但原生net/http下必须只写头、不写体
测试时容易漏掉的验证点
用 httptest.ResponseRecorder 测试 handler 时,很多人只检查响应体,却忘了断言状态码本身。
- 必须显式查
recorder.Code,而不是靠recorder.Body.String()推断 - 如果 handler 里用了
http.Error(),要确认它没被 recover 中间件拦截导致没写入 recorder - 并发测试中,goroutine 内部出错不会进主 handler 的错误处理链,得单独 mock 或加 context.Done() 控制
最常被忽略的是:状态码和业务错误码(如 JSON 里的 code 字段)属于不同层级,HTTP 状态码管通信成败,自定义 code 管业务分支——两者语义不能混,日志里也得能靠 trace_id 对齐。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











