
Go 服务端使用 json.NewEncoder(w).Encode(err) 时,若 err 为 nil,会向响应体写入 null,导致客户端读取到类似 {"foo":"bar"}null 的非法 JSON;根本原因在于错误地将 err(而非业务数据)作为 JSON 编码目标。
go 服务端使用 `json.newencoder(w).encode(err)` 时,若 `err` 为 `nil`,会向响应体写入 `null`,导致客户端读取到类似 `{"foo":"bar"}null` 的非法 json;根本原因在于错误地将 `err`(而非业务数据)作为 json 编码目标。
在 Go Web 开发中,一个常见但极易被忽视的陷阱是:误用 json.Encoder 向 HTTP 响应写入 nil 错误值。如问题所示,FooWriter 的处理逻辑中存在关键错误:
var successJSON = "{\"foo\":\"bar\"}"
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Write([]byte(successJSON))
w.WriteHeader(200) // ← 注意:此行应在 w.Write 之前或之后?实际已无效!
if err := json.NewEncoder(w).Encode(err); err != nil {
// ...
}
这段代码存在双重问题:
- w.WriteHeader(200) 调用时机错误:w.WriteHeader() 必须在任何 w.Write() 或 json.Encoder.Encode() 之前调用,否则会被忽略(net/http 会在首次写入时自动写入状态码,默认为 200)。但此处它位于 w.Write() 之后,属于冗余且易引发混淆;
- 核心缺陷:json.NewEncoder(w).Encode(err):当 err 为 nil 时,json.Encoder 会将 nil 序列化为 JSON 字面量 null,并直接写入 http.ResponseWriter 的底层 io.Writer —— 此时响应体已包含 {"foo":"bar"},null 便被追加在其后,造成 {"foo":"bar"}null,破坏 JSON 结构完整性。
✅ 正确做法是:仅对业务数据进行 JSON 编码,且确保一次响应只输出一个合法 JSON 值。例如:
// ✅ 推荐:统一使用 json.Encoder 编码结构体(类型安全、无字符串拼接风险)
type Response struct {
Foo string `json:"foo"`
Err string `json:"error,omitempty"` // 可选错误字段
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err := json.NewEncoder(w).Encode(Response{Foo: "bar"}); err != nil {
http.Error(w, "JSON encode error", http.StatusInternalServerError)
return
}
// ← 不再调用 w.WriteHeader(200):Encode 成功即隐式表示 200 OK
⚠️ 补充注意事项:
- 避免混合使用 w.Write([]byte(...)) 和 json.Encoder —— 二者均操作同一 io.Writer,极易导致响应体污染;
- 不要手动拼接 JSON 字符串(如 "{\"foo\":\"bar\"}"),既不安全又难维护;始终使用 json.Marshal 或 json.Encoder;
- 客户端无需 ioutil.ReadAll(io.LimitReader(...)) 这种复杂写法(ioutil 已弃用);推荐 io.ReadAll + 显式长度校验:
body, err := io.ReadAll(http.MaxBytesReader(nil, resp.Body, 1
总结:null 的出现不是网络或客户端问题,而是服务端逻辑错误——将 nil error 误编码为 JSON。修复的关键在于分离关注点:用 json.Encoder 编码业务响应,而非错误本身;错误应通过 HTTP 状态码或响应体中的 error 字段传达,而非独立编码。











