登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go json.Decoder流式读取大 JSON 数组的内存控制

来源:17golang原创

时间:2026-09-15 19:26:38 476浏览 收藏

大型 JSON 数组最容易踩的坑,是把整个文件读进 []byte,再用 json.Unmarshal 生成一份全量切片。数据量一上来,原始字节、解码对象和业务副本会同时占用堆。更稳妥的做法是让 json.Decoder 包住 io.Reader,消费数组边界后逐个 Decode,处理完当前元素就进入下一轮,不把结果全部追加到切片。

要点速览
  • 先用 Token 读取 [,用 More 判断是否还有元素,最后消费 ]
  • 循环变量只保留当前记录;真正的内存峰值仍受单个 JSON 元素和 Decoder 缓冲影响。
  • 大数字、未知字段和坏数据要按业务需要显式处理,不能只靠“流式”二字兜底。

先把全量解码改成逐项处理

假设文件顶层是 [{"id":1,"name":"a"}, ...]。如果业务只需要写入数据库、统计数量或转发记录,就没有必要保留所有对象。结构体接收当前元素,处理函数只拿当前值,下一次 Decode 会覆盖它。

这里的“流式”是处理策略,不等于任何大小的对象都零内存。Go 官方的 encoding/json 文档提供了数组示例;而新版官方说明也指出 v1 解码器会缓冲完整的 JSON value,所以应把目标设为“避免全量数组叠加”,并继续关注单元素大小。

用 Token 和 More 定位数组边界

Go json.Decoder 使用 Token More Decode 读取大型 JSON 数组边界的静态结构说明图
图1:静态结构说明图,查看 Token、More、Decode 与数组边界的关系。
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
)

type Event struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

func main() {
	f, err := os.Open("events.json")
	if err != nil {
		panic(err) // 打开失败时没有可继续消费的输入,直接结束示例。
	}
	defer f.Close() // 关闭文件描述符,避免长任务重复打开文件后耗尽资源。

	dec := json.NewDecoder(f)
	first, err := dec.Token()
	if err != nil {
		panic(err) // 先确认顶层确实能读出数组起始 token。
	}
	if delim, ok := first.(json.Delim); !ok || delim != '[' {
		panic("top-level JSON value is not an array") // 输入契约不符时不要误把对象当数组。
	}

	var event Event
	for dec.More() {
		event = Event{} // 清掉上一项,避免可选字段残留到当前记录。
		if err := dec.Decode(&event); err != nil {
			panic(err) // 语法错误或类型不匹配时停止,避免继续处理不可信数据。
		}
		fmt.Printf("process id=%d name=%s\n", event.ID, event.Name) // 这里替换为写库、计数或转发。
	}

	last, err := dec.Token()
	if err != nil || last != json.Delim(']') {
		panic("array is not closed") // 消费右括号,确认数组没有被截断。
	}
	_ = io.EOF // EOF 只适合判断独立 JSON 流的结束,不替代数组边界检查。
}

Token 返回的是数组分隔符,More 只应在当前数组上下文中使用。最后再读一次 Token,可以把“输入被截断”与“正常结束”区分开。示例里的 panic 只是缩短演示;服务代码应返回带上下文的错误。

在循环内处理,而不是积累结果

生产代码通常把 process(event) 换成批量写入器。批量大小可以是固定数量,但批量缓冲也要计入峰值;如果每条记录都追加到 allEvents,就重新失去了 Decoder 的主要收益。需要重试时,保留有限大小的失败队列,并给队列设上限。

选项适用场景内存与正确性提醒
Decode(&struct)字段固定、逐条处理当前元素仍需完整解码到结构体
UseNumber()金额、ID 或超大整数需保留文本精度后续要显式转换并检查错误
DisallowUnknownFields()输入契约严格、希望发现多余字段上游新增字段可能使消费端失败

核对内存边界与上线取舍

Go json.Decoder 处理大型 JSON 数组时 io.Reader 缓冲区和当前记录内存边界的静态说明图
图2:静态内存边界说明图,区分当前元素、Decoder 缓冲区与已处理数据。

排查内存时重点看三层:输入 Reader 的读取缓冲、Decoder 为当前 JSON value 保留的空间、业务处理器暂存的对象。逐项解码能消除“数组长度 × 对象大小”的长期累积,但一个异常巨大的元素仍可能造成峰值。若单元素大小也不可信,应在输入层设置字节上限,或改用能真正按 token 消费的专用方案;不要仅凭 Decoder 名称承诺无限流式。

数字默认解到 float64 可能损失大整数精度。需要保留原始数字时,在第一次读取元素前调用 dec.UseNumber()。需要严格 schema 时可调用 DisallowUnknownFields,但要先确认上游是否允许向后兼容地加字段。

相关问题

为什么用了 Decoder,内存还是会突然升高?

常见原因是单个元素很大,或业务层仍把对象追加到切片、缓存或重试队列。先分别测量元素大小和业务暂存量。

空数组需要特殊处理吗?

不需要。读到 [More 会直接返回 false,随后消费 ] 即可。

遇到数组中间的坏记录能跳过吗?

不能安全地把任意解码错误当成可跳过项。先判断语法和边界是否仍可恢复;不确定时停止并记录输入位置。

什么时候应该保留全量切片?

只有后续算法确实需要全局排序、去重或多次关联时才保留,并为数据量设置上限;单纯逐条导入不应默认全量加载。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>