应使用 json.Decoder 流式解码替代 json.Unmarshal,避免大文件 OOM;对超大数组需手动 Token() 控制解析;可选 goccy/go-json 加速;注意切片扩容与后续环节的内存管理。

用 json.Decoder 流式解码,别用 json.Unmarshal
直接 json.Unmarshal 读整个文件进内存,几 GB 的 JSON 会立刻 OOM。核心是换用 json.Decoder,它从 io.Reader(比如 *os.File)边读边解析,内存只保留当前处理的 token 或结构体。
常见错误是:先 os.ReadFile 得到 []byte,再丢给 json.Unmarshal —— 这等于把整个文件复制进内存两次(一次读,一次解析)。
- 正确做法:打开文件后传给
json.NewDecoder(f),然后反复调用Decode(&v)或手动Token()遍历 - 适用场景:JSON 是数组顶层(如
[{...},{...}]),或你能按字段名跳过无关部分 - 注意:如果顶层是对象(
{"data": [...]}),得先用Token()手动定位到"data"字段再解数组
对大数组用 Decoder.Token() 手动跳过或逐项解析
当 JSON 数组元素极多(百万级),即使流式解码每个 struct,Go 的 GC 和临时对象分配仍可能拖慢速度或抬高峰值内存。这时候要避免一次性解整个数组,改用 Token() 手动控制解析流程。
典型错误:写 var arr []MyItem; dec.Decode(&arr) —— 这会让 Decoder 自动分配并填充整个切片,内存和 GC 压力都在线性增长。
- 推荐方式:循环调用
dec.Token()判断是否为json.Delim(`[` 或 `]`),遇到 `{` 就新建一个MyItem实例,用dec.Decode(&item)解单个对象 - 好处:内存中始终只有 1 个
MyItem实例在复用(可配合item.Reset()清理),GC 压力极小 - 性能影响:比全自动解码慢约 10–20%,但内存稳定在几百 KB 级别,而非 GB 级别
用 goccy/go-json 替代标准库提升吞吐量
标准库 encoding/json 在超大文件下解析慢、反射开销大。实测中,goccy/go-json 在不改代码的前提下,仅替换 import 和函数调用,解析速度能提升 2–5 倍,且更省内存(尤其对重复字段名的 JSON)。
容易踩的坑:它默认不兼容某些自定义 UnmarshalJSON 方法,若结构体里有手写的反序列化逻辑,需加 //go:build gojson 条件编译或显式调用 json.Unmarshal 回退。
- 替换方式:import 改为
github.com/goccy/go-json,然后用json.NewDecoder(r).Decode(&v)—— 接口完全一致 - 注意:它不支持
json.RawMessage的某些边界行为,如果依赖原始字节透传,得验证 - 兼容性:Go 1.16+,无 CGO,静态链接友好
预估内存占用时,别忽略 Go 的 slice 底层扩容机制
哪怕用了流式解码,如果你在循环里不断 append 到一个切片(比如缓存一批数据批量入库),这个切片的底层数组可能因多次扩容而暂留大量已分配但未使用的内存,GC 不会立刻回收。
典型现象:top 显示 RSS 持续上涨,但 pprof 的 heap profile 里活跃对象不多 —— 很可能是切片扩容导致的“内存碎片”假象。
-
解决方法:提前预估批次大小,用
make([]T, 0, batchSize)指定 cap;处理完一批后,设batch = batch[:0]重置长度,让底层数组可复用 - 更彻底的方式:用固定大小的缓冲池(
sync.Pool)管理结构体指针,避免频繁分配 - 关键点:流式解码只是起点,后续数据流转环节(日志、缓存、网络发送)同样可能成为内存瓶颈










