应直接将io.Reader(如os.File或http.Response.Body)传给json.NewDecoder,避免先用io.ReadAll读全量再json.Unmarshal,否则丧失流式能力;解析数组需用Token()跳过[后循环Decode,并用More()判断边界;字段不确定时用json.RawMessage延迟解析以零拷贝。
为什么标准 json.Decoder 已经是流式解析器,但你可能没用对
go 标准库的 json.decoder 本身就是为流式解析设计的,它不把整个 json 加载进内存,而是边读边解析。很多人误以为它“不够快”或“不支持流式”,其实是调用方式出了问题:比如先用 io.readall 把整个 http body 读成 []byte,再传给 json.unmarshal —— 这完全绕过了流式能力。
正确做法是直接把 io.Reader(如 *http.Response.Body 或 os.File)传给 json.NewDecoder。它内部使用缓冲和状态机,单次解析一个 token,内存占用稳定在 KB 级别。
- 避免
json.Unmarshal([]byte)处理大响应体,这是最常见也是最致命的误用 - 如果需解析数组中的每个对象(如
[{"id":1}, {"id":2}]),用decoder.Token()跳过外层[,再循环decoder.Decode(&item) - 注意
decoder.More()判断是否还有元素,它比手动检查delim == '}'更可靠
如何用 json.RawMessage 延迟解析嵌套结构,避免无谓拷贝
当 JSON 中有可选字段、变长结构或只读部分字段时,提前解出完整 struct 会浪费 CPU 和内存。例如日志事件中 "payload" 字段可能是任意 JSON,而你只关心 "level" 和 "ts"。
json.RawMessage 是 []byte 的别名,它让 decoder 跳过解析,只记录原始字节位置和长度 —— 零拷贝,毫秒级延迟。
- 定义 struct 字段为
Payload json.RawMessage `json:"payload"`,后续按需用json.Unmarshal(payload, &target) - 不要对
json.RawMessage做字符串拼接或截取,它的字节依赖原始 reader 缓冲;若需长期持有,应显式append([]byte{}, raw...) - 与
interface{}相比,RawMessage解析快 3–5 倍,且内存更可控
遇到 invalid character 'x' looking for beginning of value 怎么快速定位?
这个错误几乎都源于 reader 提前被消费或数据损坏,不是 JSON 语法问题。典型场景:HTTP response body 被多次读取、gzip 解压后未重置 reader、或粘包导致开头混入非 JSON 数据(如 HTTP header 剩余字节)。
- 用
io.TeeReader(r, os.Stderr)把原始输入 dump 到终端,一眼看到错在哪一行哪一列 - 确认 reader 是否实现了
io.Seeker;如果不是(如net.Conn),就不能反复调用decoder.Decode试图重试 - 如果上游返回的是
application/json; charset=utf-8,但实际含 BOM(\uFEFF),需在创建 decoder 前用bytes.TrimPrefix(buf, []byte("\uFEFF"))清理
要不要自己写基于 encoding/json 的封装?通常不用
除非你有极特殊需求(如解析超深嵌套时控制栈深度、或需要自定义浮点精度策略),否则不建议封装 json.Decoder。标准库已足够健壮,且 Go 1.20+ 对小对象解析做了逃逸优化,Decode(&v) 中的 v 很少逃逸到堆上。
- 想提升吞吐?优先调大
bufio.NewReaderSize(r, 64 的缓冲区,比换库见效更快 - 想减少 GC?避免在 hot path 上 new struct,复用
sync.Pool中的解码目标变量 - 真正卡性能的往往不是解析本身,而是后续的字段校验、时间解析、或 map 查找 —— 先 profile 再动手
流式解析真正的复杂点不在“怎么写”,而在“怎么保证 reader 生命周期和业务逻辑对齐”:body 关闭时机、错误恢复策略、以及当解析中途失败时,如何安全丢弃剩余字节而不阻塞连接。这些细节比选什么库重要得多。



















