不能直接用 json.Decoder.Decode(&[]T) 做流式解析,因其仍会全量分配内存、触发反射开销,且易因未检查 dec.More() 导致解析错误;正确做法是手动控制 token 流或封装支持 NDJSON 与 JSON 数组的零拷贝 StreamReader,结合按需解析(如 Token 状态机或 RawMessage)和严格边界校验。

为什么不能直接用 json.Decoder.Decode(&[]T) 做流式解析
它看起来像流式,实则仍是全量分配:decoder.Decode(&[]YourStruct{}) 会一次性 new 出整个 slice,100 万条记录就 malloc 100 万个 struct + slice 底层数组扩容冗余空间。内存峰值和 json.Unmarshal 几乎无差别,根本没绕过反射和临时对象开销。
常见错误现象:invalid character '}' after top-level value,本质是 decoder 在数组末尾多调了一次 Decode(),因为没检查 dec.More() 就直接循环。
- 正确姿势是手动控制 token 流:先
dec.Token()确认开头是'[',再进for dec.More() { ... }循环 - 循环体第一行必须加
if !dec.More() { break },防 EOF 后误读 - 每个
dec.Decode(&item)前确保 reader 位置在下一个 JSON 对象起始处——这依赖上游已按帧切分
如何封装一个支持 NDJSON 和 JSON 数组的通用流式 Reader
NDJSON(每行一个 JSON)和顶层 JSON 数组([{...},{...}])语义不同,但底层都需要“按值边界切片”。不能共用同一套 json.Decoder 实例,得抽象出统一的 Next() 接口。
核心是把原始字节流交给 findJSONValueBounds() 扫描,返回 [start, end) 切片,再用 bytes.NewReader(buf[start:end]) 构造新 reader 丢给 json.NewDecoder() 单次解析。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 对 NDJSON:用
bufio.Scanner按行切分,每行调一次findJSONValueBounds() - 对 JSON 数组:扫描时维护括号栈,识别第一个
{到匹配的}为一个完整对象 - 所有切片都基于原 buffer subslice,零拷贝;buffer 预分配 4KB,复用避免高频 alloc
- 暴露
func (r *StreamReader) Next() ([]byte, error),下游自己决定用json.Unmarshal还是gjson.GetBytes
怎样让结构体字段解析真正“按需”而非“全量”
哪怕用了流式 Reader,如果每次 json.Unmarshal 都解到完整 struct,仍会触发反射、分配 map、拷贝字符串——性能瓶颈不在 IO,在解码器本身。
真正按需 = 只读目标字段,跳过其余全部内容。这必须放弃 Unmarshal,改用 Token() 手动状态机,或用 json.RawMessage 延迟解析。
- 固定字段场景(如只取
"id"和"ts"):用decoder.Token()循环,遇到目标 key 后立刻decoder.Token()读值,其他 key 全用skipValue(dec)跳过 - 嵌套路径(如
"data.payload.size"):记录当前层级深度和路径匹配状态,进入"data"后才开始监听"payload",再下一层才提取"size" - 大字段/动态字段场景:struct 中声明
Raw json.RawMessage字段,不解析、不拷贝,后续真需要时再json.Unmarshal(Raw, &sub)
模块封装时最容易被忽略的三个边界点
封装成可复用模块后,90% 的线上问题出在边界处理上,不是逻辑错,而是漏校验。
-
findJSONValueBounds()必须处理字符串内转义引号("a\"b"),否则在{"name":"foo\"bar"}里提前截断 - 从
net.Conn或http.Response.Body构造 reader 时,若服务端启用了 gzip,必须先gzip.NewReader(r)解压,否则findJSONValueBounds()扫描的是压缩字节流,必然失败 - 预分配 buffer 大小要设硬上限(如 2MB),并在
findJSONValueBounds()返回前校验end - start ,防恶意构造超长 JSON 值耗尽内存
流式解析的“高性能”从来不是靠单点优化,而是整条链路上每一帧都做长度控制、每一层都拒绝未校验输入、每一个 subslice 都明确生命周期——否则任何一处松动,都会在高并发下放大成雪崩。


















