
本文介绍如何在 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("%+vn", 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 的数组流场景,放弃“魔改标准库解码器”的思路,转向成熟、专注的流式词法分析器,是平衡开发效率、运行性能与长期可维护性的最优解。