json.Unmarshal易导致OOM,应改用json.NewDecoder流式解析;NDJSON需直接解码;JSON数组须手动跳括号并用dec.More()控制循环;动态字段用json.RawMessage零拷贝处理。

json.Unmarshal 会直接 OOM,别在微服务里用它解析请求体
微服务接收的 JSON 请求体(比如日志上报、批量事件、ETL 数据)动辄几十 MB,json.Unmarshal 要求传入完整 []byte,意味着你必须先调用 io.ReadAll(r.Body) —— 这等于把整个请求体拷进内存,GC 压力陡增,高频请求下秒变 OOM 火药桶。
正确姿势是让 json.NewDecoder 直接绑定 r.Body(http.Request.Body 本身就是一个 io.ReadCloser):
func handler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() // 必须显式关闭
dec := json.NewDecoder(r.Body)
for {
var event map[string]interface{}
if err := dec.Decode(&event); err == io.EOF {
break
} else if err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
// 处理单个 event
}
}
-
r.Body可能被中间件提前读取(如 gzip 中间件),此时再解码会返回io.EOF或空数据 —— 检查r.Header.Get("Content-Encoding"),必要时用decompress.NewReader包一层再传给json.NewDecoder - 别在循环里反复 new
json.Decoder,复用一个实例即可;但注意它不是并发安全的,每个请求用独立 decoder - 如果客户端发的是
application/json; charset=utf-8,decoder 默认支持 UTF-8,无需额外处理 BOM;但若含 BOM 且首字节是0xEF 0xBB 0xBF,Decode()会报invalid character ''—— 此时需用bytes.TrimPrefix预处理(仅限小请求体,大请求体别全读)
解析 NDJSON 流(每行一个 JSON)时别用 bufio.Scanner
很多微服务接收的是 NDJSON 格式:一行一个 JSON 对象,例如日志推送或 webhook 批量事件。常见错误是用 bufio.Scanner 逐行读取,再对每行调用 json.Unmarshal —— 这样每行仍要分配内存,且 Scanner 默认 64KB 缓冲区可能被超长行撑爆。
真正轻量的做法是跳过行边界,直接把 r.Body 交给 json.NewDecoder:
立即学习“go语言免费学习笔记(深入)”;
dec := json.NewDecoder(r.Body)
for {
var item struct {
ID int `json:"id"`
Action string `json:"action"`
Data json.RawMessage `json:"data"`
}
if err := dec.Decode(&item); err == io.EOF {
break
} else if err != nil {
log.Printf("parse line error: %v", err)
continue // 错误行跳过,不中断后续
}
// 处理 item
}
-
json.NewDecoder对 NDJSON 天然友好:每调用一次Decode()就消费一个顶层 JSON 值,自动跨行也无妨(只要 JSON 合法) -
json.RawMessage字段用于暂存未结构化的部分,避免反序列化开销;但注意它不能作为map[string]json.RawMessage的 value 类型(会 panic),只能放在 struct 字段或 slice 元素里 - 某行 JSON 语法错误(如少引号)会导致
err不是io.EOF,必须显式continue或break,否则循环卡死
解析顶层为 JSON 数组的请求体必须手动跳括号
当客户端 POST 一个巨型 JSON 数组(如 [{"id":1}, {"id":2}, ...])时,直接 dec.Decode(&[]T{}) 会让 decoder 尝试把整个数组加载成 Go slice —— 内存又爆了。这是微服务中最常踩的坑。
必须用 Token() 手动吃掉 [,再靠 dec.More() 控制循环:
dec := json.NewDecoder(r.Body)
tok, err := dec.Token()
if err != nil || tok != json.Delim('[') {
http.Error(w, "expected '['", http.StatusBadRequest)
return
}
for dec.More() {
var item struct {
ID int `json:"id"`
}
if err := dec.Decode(&item); err != nil {
log.Printf("decode item failed: %v", err)
continue
}
// 处理 item
}
// dec.More() 返回 false 后,下一个 Token 是 ']',可选检查
if _, err := dec.Token(); err != nil && err != io.EOF {
log.Printf("expect ']', got error: %v", err)
}
-
dec.More()只在数组或对象内部有效;调用前必须已进入容器(即已读到[或{) - 别依赖
io.EOF退出循环 ——dec.More()返回false才是数组结束的明确信号;若不检查就继续Decode(),会报invalid character '}' after top-level value - 如果数组元素结构不一致(比如混着
{"type":"user"}和{"type":"order"}),统一用map[string]interface{}解,再根据type字段分支处理,比定义多个 struct 更灵活
字段动态或含脏数据时,json.RawMessage 是保命符
微服务对接外部系统时,JSON 字段经常不稳定:同一字段有时是字符串、有时是数字;或新版本加了字段、旧版本缺失;甚至字段值是 base64 图片或 HTML 片段 —— 全量反序列化极易 panic 或卡死。
把不确定字段声明为 json.RawMessage,只存字节引用,不触发解析:
type Event struct {
ID int `json:"id"`
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // 不解析,留作原始字节
Metadata json.RawMessage `json:"metadata"`
}
func (e *Event) GetPayload() (map[string]interface{}, error) {
if len(e.Payload) == 0 {
return nil, nil
}
var m map[string]interface{}
return m, json.Unmarshal(e.Payload, &m)
}
-
json.RawMessage底层只是原 JSON 字节的 subslice,零拷贝,内存开销极小 - 它不能用于
map[string]json.RawMessage的 value(Go runtime 会 panic),只能作为 struct 字段或 slice 元素 - 如果字段名本身是动态的(比如
"user_123"、"org_456"),必须用map[string]json.RawMessage,但要限制 key 长度(如len(key) > 256就丢弃),防恶意超长 key 耗尽内存
流式解析不是“选配”,而是微服务吞吐和稳定性的底线 —— 一旦请求体超过几 MB,json.Unmarshal 就不再是选项,而是定时炸弹。关键不在代码多几行,而在是否愿意放弃“一次性全拿”的惯性思维,转而信任 token 状态机和按需消费的节奏。


















