go 中无法对 json 字符串进行字节级“分页”加载,因其嵌套结构特性导致任意截断必破坏语法完整性;正确做法是使用 json.decoder 配合 token() 和 more() 实现状态机式流式解析:先跳过 '[',再循环 decode 单个元素,配合 json.rawmessage 延迟解析大字段,避免 oom。

Go 没有“内存分页加载 JSON 字符串”这回事——json.Unmarshal 从不支持分页,它只认完整、合法的 JSON 值(对象或数组),强行切片传入会报 invalid character 或 unexpected end of JSON input。所谓“分页”,本质是放弃一次性解析,改用流式 Token 驱动。
为什么不能对 JSON 字符串做“分页”?
JSON 是嵌套结构,不是线性文本。一个 { 可能跨多行、嵌套十几层,中间夹着字符串转义、注释(虽标准不支持但实际常见)、超长 base64 字段——你根本无法安全地按字节偏移“切一刀”。json.Unmarshal 输入必须是语法完整的最小单元,比如单个对象 {"id":1} 或单个数组项 {"name":"a"},而不是“前 1MB 字节”。
- 试图
json.Unmarshal([]byte(jsonStr[:n]), &v):几乎必然 panic,因为截断点大概率落在字符串中间、括号里或转义序列中 - 用正则或字符串查找
}位置来“分页”:遇到{"msg": "a}b"}就失效,JSON 允许任意字符在字符串内 - 把大 JSON 当纯文本按行/按块读再拼:仍需等完整结构才能调
Unmarshal,没解决 OOM,只是延迟了崩溃点
真正可行的替代方案:json.Decoder + Token 手动推进
这不是“分页”,而是状态机式消费:跳过容器符号,逐个提取成员,每处理完一个就丢弃引用。适用于顶层是数组的场景(如 [{...},{...},...])。
- 先
dec.Token()检查并消耗[,确认是数组开始 - 进
for dec.More() { ... }循环,每次dec.Decode(&item)只解一个对象,内存只保留当前item - 循环外别写
dec.Decode(&[]T{})—— 它仍会分配整个切片底层数组,和Unmarshal一样 OOM - 若字段值很大(如日志内容、base64 图片),用
json.RawMessage延迟解析,避免立即解码成字符串
超长单行 JSON(如 JSONL 或巨型对象)怎么读?
如果文件本身是一整块 JSON(非数组),且单行超过默认 64KB,bufio.Scanner 会直接 panic:scanner: token too long。
- 必须显式调
scanner.Buffer(make([]byte, 1024*1024), 1024*1024),第二个参数设为最大容忍长度(如 1MB) - 更稳妥:换
bufio.Reader+ReadBytes('\n')或ReadString('\n'),自己控制读取边界 - 读到行后,用
json.NewDecoder(strings.NewReader(line)).Decode(&v),而非json.Unmarshal([]byte(line), &v)—— 避免额外一次字节复制 - 处理完立刻让
line = "",切断引用,助 GC 回收
最容易被忽略的内存泄漏点
流式处理不等于自动防 OOM。很多代码看似用了 Decoder,但仍在循环里累积数据:
- 把每个
itemappend 进全局[]T切片:底层数组会持续扩容,内存只增不减 - 闭包捕获了
item并异步发往 channel 或 DB:只要 channel 未消费,item就不会被 GC - 用
map[string]*T缓存历史记录却忘了清理:key 不删,value 永远存活 - 忘记
defer file.Close()或decoder所依赖的*os.File未关闭:文件句柄+内核缓冲区持续占用
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











