json.unmarshal会oom是因为它需一次性将整个json字符串加载并解析为go结构体切片,内存占用与数据规模线性正比,无法流式处理或提前丢弃已处理项。

为什么 json.Unmarshal 会 OOM?
直接用 json.Unmarshal 加载几百 MB 的 JSON 数组,几乎必然触发内存溢出。它会把整个 JSON 字符串解析成 Go 的 []interface{} 或结构体切片,中间拷贝多、无流式控制、无法提前丢弃已处理项。
真正的问题不是“怎么解析”,而是“怎么避免一次性全量加载”。关键路径是:文件 → 流式读取 → 边解析边处理 → 不保留原始数据副本。
- JSON 数组顶层必须是
[...],不能是多个独立 JSON 对象拼接(那是 NDJSON) - 如果数组元素结构统一,优先定义具体 struct,避免用
map[string]interface{}—— 后者字段名字符串和嵌套 map 开销极大 - 别在
for range里对每个元素做深拷贝或缓存到全局 slice;处理完就丢
用 json.Decoder 配合 Token() 手动跳过数组头尾
标准库 json.Decoder 支持按 token 流式读取,适合大数组。但它的 Decode() 方法默认要求顶层是单个值,遇到 [ 就报错 json: cannot unmarshal array into Go value of type ...。得手动消费开头的 [ 和结尾的 ]。
示例逻辑:
dec := json.NewDecoder(f)
tok, _ := dec.Token() // 读到 '['
for dec.More() { // 每次迭代对应一个数组元素
var item MyStruct
if err := dec.Decode(&item); err != nil {
// 处理单条解析失败,不要中断整个流
continue
}
process(item) // 立即处理,不存
}
tok, _ = dec.Token() // 读到 ']'
-
dec.More()是关键——它只在下一个 token 是,或]时返回 true,内部自动跳过空白和分隔符 - 如果 JSON 文件带 BOM 或首尾空格,
dec.Token()可能先读到json.TokenString类型的空白,需循环跳过 - 别在循环里反复 new
json.Decoder,复用一个实例即可;它本身不保存数组状态
当数组嵌套太深或字段动态时,用 json.RawMessage 延迟解析
如果某字段内容巨大(如 base64 图片、长文本),或结构不确定(API 返回字段随版本变化),硬解成 struct 会浪费内存。用 json.RawMessage 把那段 JSON 字节原样截下来,需要时再单独解。
例如:
type LogEvent struct {
ID string `json:"id"`
Data json.RawMessage `json:"data"` // 不解析,仅保留字节
Ts int64 `json:"ts"`
}
// 后续按需解析:
var payload map[string]interface{}
if err := json.Unmarshal(event.Data, &payload); err != nil {
// ...
}
-
json.RawMessage本质是[]byte,不分配新内存,但注意它指向原始 buffer —— 如果 decoder 用的是bytes.Reader或strings.Reader,没问题;若用os.File,需确保文件未被其他 goroutine 修改 - 延迟解析不减少初始读取时间,但显著降低峰值内存 —— 尤其当 90% 的
Data字段实际不会被访问时 - 别对
json.RawMessage做字符串拼接或 fmt.Printf,它不含结束符,可能截断
生产环境必须加限速与错误恢复
大 JSON 文件常因网络中断、磁盘损坏或编码错误中途失败。单纯靠 io.EOF 判断不够 —— 可能卡在某个字段的 UTF-8 解码失败,或遇到非法控制字符。
- 给
json.Decoder设置dec.DisallowUnknownFields(),避免未知字段悄悄吃掉内存 - 用
io.LimitReader包裹文件 reader,防止恶意超长字段(如重复的"key":"value"占满内存) - 记录当前已处理的数组索引(比如每 1000 条写一次 checkpoint),崩溃后可从断点 resume,而不是重头来
- 如果下游处理慢,考虑加 buffer channel 控制并发,别让 decoder 跑太快把内存撑爆
最易被忽略的是:数组元素间没有天然边界标记,一旦某个元素解析失败,dec.More() 可能卡住或跳到错误位置。务必在每次 dec.Decode() 后检查 err,并用 dec.Token() 手动同步到下一个 { 或 [ —— 这步调试起来很麻烦,但线上稳定运行依赖它。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











