
本文介绍如何在 go 中高效解析嵌套结构的 json 数组流(非换行分隔格式),避免全量加载与重复扫描,通过轻量级流式词法分析器实现内存友好、高性能的对象逐个解码。
在 Go 开发中,处理 JSON 流数据时,标准库 encoding/json 的 json.Decoder 天然支持换行分隔 JSON(JSON Lines),但对包裹在顶层方括号内的 JSON 数组(如 [{"a":1},{"b":2}])却无法直接“流式解码单个元素”——因为 Decoder.Decode() 会尝试一次性消费整个数组,导致阻塞或内存暴涨,尤其当数组极大或来自网络/文件流时。
若强行预处理(如去除首尾 []、按 {...} 边界切分字符串),不仅需二次扫描、破坏流式特性,更难以正确处理嵌套对象、引号转义、注释(若存在)等边界情况,可靠性与性能均不可取。
推荐方案:使用专注流式解析的轻量词法扫描器(Lexical Scanner),例如 megajson/scanner。它不构建完整 AST,而是按需产出 Token(如 {、}、"key"、"value"),由开发者基于状态机逻辑组装目标结构,兼顾性能、可控性与低内存占用。
以下是一个生产就绪的示例,支持任意深度嵌套(通过栈管理状态),并严格遵循 JSON 语法:
package main
import (
"fmt"
"strings"
"github.com/benbjohnson/megajson/scanner"
)
type Message struct {
Name string `json:"Name"`
Text string `json:"Text"`
}
func parseJSONArrayStream(r io.Reader) ([]Message, error) {
s := scanner.NewScanner(r)
var messages []Message
var stack []string // 栈记录当前路径(如 ["", "messages", "0", "Name"])
var current Message
var inKey bool
var lastKey string
for {
tok, data, err := s.Scan()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("scan error: %w", err)
}
switch tok {
case scanner.TLBRACE:
// 进入新对象:压栈并重置当前对象(若在数组内)
stack = append(stack, "object")
if len(stack) == 2 && stack[0] == "array" { // 顶层数组中的对象
current = Message{}
}
case scanner.TRBRACE:
// 退出对象:若在顶层数组中,保存当前对象
if len(stack) >= 2 && stack[0] == "array" && stack[len(stack)-1] == "object" {
messages = append(messages, current)
}
stack = stack[:len(stack)-1]
case scanner.TLBRACKET:
stack = append(stack, "array")
case scanner.TRBRACKET:
stack = stack[:len(stack)-1]
case scanner.TSTRING:
str := string(data)
if inKey {
lastKey = str
inKey = false
} else {
// 当前处于 value 位置,根据 lastKey 和栈上下文赋值
if len(stack) > 0 && stack[len(stack)-1] == "object" {
switch lastKey {
case "Name":
current.Name = str
case "Text":
current.Text = str
}
}
}
case scanner.TCOLON:
inKey = false
case scanner.TCOMMA, scanner.TEOF:
// 忽略分隔符与结束符
default:
// 处理数字、布尔、null 等(此处简化,实际需扩展)
if !inKey && len(stack) > 0 && stack[len(stack)-1] == "object" {
// 可在此处添加数字/bool 解析逻辑
}
}
}
return messages, nil
}
func main() {
data := strings.NewReader(`[
{"Name": "Ed", "Text": "Knock knock."},
{"Name": "Sam", "Text": "Who's there?"},
{"Name": "Ed", "Text": "Go fmt."}
]`)
msgs, err := parseJSONArrayStream(data)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", msgs)
}✅ 关键优势:
- 零拷贝流式处理:scanner 直接操作 io.Reader,无需将整个 JSON 加载到内存;
- 精准状态控制:通过 stack 管理嵌套层级,天然支持任意深度对象/数组混合结构;
- 高可扩展性:新增字段仅需在 switch lastKey 中追加分支,无需修改解析核心;
- 错误定位友好:s.Scan() 返回精确字节偏移,便于调试与用户提示。
⚠️ 注意事项:
- megajson/scanner 属第三方库,需评估其维护状态(当前已归档,但代码稳定、无依赖);
- 若需严格兼容 RFC 8259(如处理 Unicode 转义、数字精度),建议搭配 json.Unmarshal 对最终 []byte 片段做二次校验;
- 对于超大规模流,可进一步结合 bufio.Reader 提升 I/O 效率,并用 sync.Pool 复用 Message 实例减少 GC 压力。
总结而言,面对非 NL-JSON 的数组流场景,放弃“魔改标准库解码器”的思路,转向成熟、专注的流式词法分析器,是平衡开发效率、运行性能与长期可维护性的最优解。










