json.decoder支持流式解析,内存占用稳定在kb级;json.unmarshal需加载全部json到内存,易导致oom。

直接用 json.Decoder 从 *os.File 或 io.Reader 逐条解析,别碰 json.Unmarshal——后者会把整个文件读进内存,几 GB 的 JSON 数组秒变 OOM。
为什么 json.Decoder 能流式,而 json.Unmarshal 不能
json.Unmarshal 必须接收完整的 []byte,意味着你得先调用 os.ReadFile 或 io.ReadAll 把全部内容加载进内存;json.Decoder 则绑定在任意 io.Reader 上,内部用缓冲 + 状态机,每次只解析一个 JSON 值(比如一个 {} 或 "string"),内存占用稳定在 KB 级别。
常见误用:body, _ := io.ReadAll(resp.Body); json.Unmarshal(body, &v)——这完全绕过了流式能力,和用 Python 一次性 json.load(open(...)) 一样危险。
- 真正流式起点:传
*os.File、http.Response.Body、bufio.Reader给json.NewDecoder -
json.Unmarshal适合配置文件、小响应体;json.Decoder才是日志、ETL、API 流的标配 - 性能差异不是“快一点”,而是“OOM 还是稳住”
如何解析 NDJSON(每行一个 JSON)
NDJSON 是最省心的流式格式:{"id":1,"name":"a"}{"id":2,"name":"b"}。它天然适配 json.Decoder.Decode(),无需切分、无需跳括号。
关键点:别用 bufio.Scanner 一行行读再丢给 json.Unmarshal——那又回到按行分配内存的老路;正确做法是直接把文件句柄交给 Decoder:
用于端到端视频本地化流程的轻量编排器,路由至四个专注子技能——/wjs-transcribing-audio、/wjs-translating-subtitles...
file, _ := os.Open("data.ndjson")
defer file.Close()
dec := json.NewDecoder(file)
for {
var item map[string]interface{}
if err := dec.Decode(&item); err == io.EOF {
break
} else if err != nil {
log.Fatal(err) // 注意:语法错误、类型不匹配都会在这里暴露
}
// 处理 item
}
- 错误现象
invalid character 'N' looking for beginning of value:通常是文件开头有 BOM 或空行,可用bytes.TrimLeft预处理首段(但仅限小文件,大文件别全读) -
Decoder.DisallowUnknownFields()仍生效,但它是对每一行单独校验 - 如果某行 JSON 格式错误,
err不是io.EOF,必须显式处理,否则循环卡死或 panic
如何解析顶层是 JSON 数组的大文件(如 [{},{},...])
这是最易踩坑的场景:json.Decoder.Decode(&slice) 会试图把整个数组解成 Go slice,又爆内存;想逐个解析,必须手动跳过 [,再靠 dec.More() 控制循环边界。
核心逻辑:先用 Token() 吃掉左括号,再进 for dec.More() 循环,每次 Decode() 一个元素:
dec := json.NewDecoder(file)
tok, _ := dec.Token()
if tok != json.Delim('[') {
log.Fatal("expected [")
}
for dec.More() {
var item MyStruct
if err := dec.Decode(&item); err != nil {
log.Fatal(err)
}
// 处理 item
}
-
dec.More()只在数组/对象内部有效,调用前必须已进入容器(比如用Token()吃掉[,或先Decode(&dummy)进入) - 不能在
dec.More()外直接Decode(),否则第一次就试图解整个数组 - 数组成员结构不一致时(如混合
{"type":"user"}和{"type":"event"}),建议先用map[string]interface{}解,再根据type字段分支处理 - 嵌套深或字段动态时,
json.RawMessage可延迟解析,避免无谓的结构体分配
容易被忽略的三个细节
流式解析不是设了 Decoder 就万事大吉——漏掉这些,轻则解析错位,重则 goroutine 泄漏或静默丢数据。
- 必须检查
err == io.EOF作为正常退出条件;其他err(如invalid character '}')代表解析位置错乱,大概率是上一轮失败后没中断循环,继续Decode() - 复用
Decoder时注意它内部有缓冲;不要把同一个io.Reader(如*bytes.Reader)反复传给多个Decoder实例 - 如果输入来自
gzip.NewReader或zstd.NewReader,记得在解析完后显式调用.Close(),否则底层 reader 可能泄漏
真正难的不是写对第一行 json.NewDecoder,而是确保 token 边界不漂移、错误不吞没、资源不残留——这些地方一出问题,往往没有明显报错,只有数据少了几千条。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










