json.Unmarshal不能用于大批量JSON,因为它必须先将整个文件加载进内存再反射解析,2GB文件会导致4–6GB内存占用而必然OOM;正确做法是用json.Decoder配合Token()跳过'['、dec.More()控制循环、逐个Decode(&item)实现真流式解析。

为什么 json.Unmarshal 不能用于大批量 JSON
它必须先把整个文件读进内存,再反射解析。一个 2GB 的 JSON 文件,json.Unmarshal 至少会占用 4–6GB 内存——不是“可能 OOM”,而是“必然被系统 kill”。常见错误现象是进程突然消失,dmesg 里出现 Out of memory: Kill process。
别写这种代码:data, _ := os.ReadFile("big.json"); json.Unmarshal(data, &v)。哪怕你只想要前 10 条,它也得全 load 进来。
-
os.ReadFile本身就会阻塞并分配超大 slice -
json.Unmarshal再做一次深度拷贝和反射,内存峰值翻倍 - GC 扫描压力剧增,导致 STW 时间飙升,服务响应卡顿
用 json.Decoder 做真流式解析的三步铁律
核心不是“用了 Decoder”,而是是否手动控制 token 流。顶层是数组时,decoder.Decode(&[]T{}) 仍是假流式——它会一次性分配底层数组,照样爆内存。
正确做法只有三步,缺一不可:
立即学习“go语言免费学习笔记(深入)”;
- 调一次
dec.Token(),确认并跳过'['(返回值必须是json.Delim('[')) - 用
for dec.More() { }循环,dec.More()才是数组边界权威判断 - 每次循环内只调一次
dec.Decode(&item),item类型可以是 struct、map[string]interface{}或json.RawMessage
漏掉 dec.More() 或写成 for { if err == io.EOF { break } },就会在最后一个元素后多 decode 一次,报错 invalid character '}' after top-level value——这不是语法错,是控制逻辑错。
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
字段不确定时,json.RawMessage 是唯一安全出口
日志、ETL 数据常混着不同 "type" 的对象,硬写 struct 容易 panic;用 interface{} 又会触发反射 + 全量解析,内存和 CPU 都扛不住。
把动态字段声明为 json.RawMessage,只存原始字节引用,不解析:
type LogEntry struct {
ID int `json:"id"`
Type string `json:"type"`
Metadata json.RawMessage `json:"metadata"`
}
后续按需解析:json.Unmarshal(entry.Metadata, &target)。注意:json.RawMessage 不能直接用在 map[string]interface{} 的 value 上(会 panic),只能用于 struct 字段或 slice 元素。
- 避免
map[string]json.RawMessage时 key 过长,需限制长度防恶意构造 - 若字段可能为空或 null,
json.RawMessage会保留null字节,解码时要处理len(raw) == 0或string(raw) == "null"
HTTP 场景下最容易被忽略的两个细节
直接传 resp.Body 给 json.NewDecoder 看似没问题,但实际常因两个细节失败:
- 上游返回 gzip 压缩内容,但没解压就喂给 decoder ——
json.SyntaxError报的是乱码,真实原因是未用gzip.NewReader(resp.Body)包一层 - 响应开头带 BOM(
EF BB BF)或空行,dec.Token()第一次就读到非法字符 —— 不要用bytes.TrimLeft全读,而应在bufio.NewReader后手动 peek 前几个字节跳过 BOM
NDJSON(每行一个 JSON)反而最省心:json.NewDecoder(scanner) 直接循环 Decode(&item) 即可,不用管括号和逗号,但要注意单行格式错误会导致 err 不是 io.EOF,必须显式处理,否则循环卡死。

















