Go encoding/json Decoder Token 流式读取嵌套结构
用 encoding/json.Decoder.Token 读取嵌套 JSON 时,我更倾向于让 Token 只负责“走到正确的容器边界”,再让 Decode 负责当前那一个结构体。这样既不需要把整份文档解码成 map[string]any,也不必手工处理每个标量:顶层对象逐字段读取,目标数组逐元素解码,处理完一条就交给回调。
稳定的组合是:先用
Token()消费{、字段名和[,在容器内部用More()判断是否还有成员,用Decode(&item)物化当前元素,最后显式消费匹配的]与}。未知字段如果是对象或数组,必须完整跳过整个值。
官方文档:https://pkg.go.dev/encoding/json
Token返回标量或json.Delim,并保证对象、数组分隔符正确嵌套匹配。More只用于已经进入的数组或对象,不能拿它判断整个输入流是否结束。- 大数组可以逐元素
Decode;单个元素仍会被完整解码,所以内存边界是“一个元素”,不是零分配。 - 数字需要保留原始十进制文本时先调用
UseNumber,错误位置可结合InputOffset记录。
先确定只保留哪些数据
我在设计这类解析器时,第一步不是马上写 for dec.More(),而是先画出数据生命周期。假设顶层对象包含小型 meta、可能很大的 items 数组,以及未来可能增加的 debug、extensions 等字段。目标是把 meta 留在内存里,把 items 一条条交给存储或计算函数,其他字段跳过。
这个边界很重要:如果先 Decode 到包含 []Item 的总结构体,数组还是会整体驻留内存;如果所有内容都按 Token 拆到标量级,代码又会充满类型断言和层级状态。实际工程里,容器层用 Token、业务对象用 Decode,通常是可读性和内存占用之间更舒服的折中。
| 数据部分 | 读取方式 | 生命周期 |
|---|---|---|
| 顶层对象 | Token + More | 只维护当前字段位置 |
| meta | Decode 到小结构体 | 文档处理期间保留 |
| items | 数组边界用 Token,元素用 Decode | 每次只保留当前元素 |
| 未知字段 | 递归 skipValue | 消费后立即丢弃 |
Token 负责边界,Decode 负责当前元素
Token() 会返回下一个 JSON token。对象和数组的四个分隔符以 json.Delim 返回;对象键和字符串值是 string,布尔值是 bool,null 是 nil。逗号和冒号不会作为 token 返回。进入 { 或 [ 后,More() 才表示当前容器内是否还有下一个成员。
我会先写一个很小的边界函数,所有容器入口和出口都经过它。这样代码遇到结构不符时,错误不会退化成难懂的类型断言失败。
func expectDelim(dec *json.Decoder, want json.Delim) error {
// 只接受指定的容器边界,并带上当前字节偏移。
tok, err := dec.Token()
if err != nil {
return fmt.Errorf("读取分隔符失败(offset=%d): %w", dec.InputOffset(), err)
}
delim, ok := tok.(json.Delim)
if !ok || delim != want {
return fmt.Errorf("期望分隔符 %q,实际为 %v(offset=%d)", want, tok, dec.InputOffset())
}
return nil
}

完整解析器:逐字段进入,逐元素交付
下面的解析器要求顶层必须是对象,items 必须是数组,并拒绝重复的 items 字段。每个数组元素解码后立即调用 handle;回调可以写数据库、累计统计或发送到下一阶段。回调失败时立即停止继续读取,让上层决定重试或记录失败。
type Meta struct {
TraceID string `json:"trace_id"`
Source string `json:"source"`
}
type Item struct {
ID json.Number `json:"id"`
Tags []string `json:"tags"`
Payload map[string]string `json:"payload"`
}
func parseDocument(r io.Reader, handle func(Item) error) (Meta, error) {
dec := json.NewDecoder(r)
dec.UseNumber() // 动态数字和 Token 数字保留为 json.Number。
var meta Meta
seenItems := false
// 顶层必须从对象开始。
if err := expectDelim(dec, '{'); err != nil {
return meta, err
}
for dec.More() {
// 对象中的下一个 Token 应当是字段名。
keyToken, err := dec.Token()
if err != nil {
return meta, fmt.Errorf("读取字段名失败(offset=%d): %w", dec.InputOffset(), err)
}
key, ok := keyToken.(string)
if !ok {
return meta, fmt.Errorf("对象键不是字符串(offset=%d)", dec.InputOffset())
}
switch key {
case "meta":
// meta 很小,直接解码成结构体更清楚。
if err := dec.Decode(&meta); err != nil {
return meta, fmt.Errorf("解码 meta 失败(offset=%d): %w", dec.InputOffset(), err)
}
case "items":
if seenItems {
return meta, fmt.Errorf("items 字段重复(offset=%d)", dec.InputOffset())
}
seenItems = true
// 进入数组后,每次只解码当前元素。
if err := expectDelim(dec, '['); err != nil {
return meta, err
}
for dec.More() {
var item Item
if err := dec.Decode(&item); err != nil {
return meta, fmt.Errorf("解码 item 失败(offset=%d): %w", dec.InputOffset(), err)
}
if err := handle(item); err != nil {
return meta, fmt.Errorf("处理 item %s 失败: %w", item.ID, err)
}
}
if err := expectDelim(dec, ']'); err != nil {
return meta, err
}
default:
// 未知字段可能是标量、对象或数组,必须跳过完整值。
if err := skipValue(dec); err != nil {
return meta, fmt.Errorf("跳过字段 %q 失败(offset=%d): %w", key, dec.InputOffset(), err)
}
}
}
if err := expectDelim(dec, '}'); err != nil {
return meta, err
}
if !seenItems {
return meta, errors.New("缺少 items 字段")
}
// 顶层对象后只允许空白,发现第二个值时拒绝尾随数据。
if tok, err := dec.Token(); err != io.EOF {
if err != nil {
return meta, fmt.Errorf("检查文档结尾失败(offset=%d): %w", dec.InputOffset(), err)
}
return meta, fmt.Errorf("对象后存在尾随 token %v(offset=%d)", tok, dec.InputOffset())
}
return meta, nil
}
这里最让我觉得实用的点,是 Decode 可以和 Token 在同一个 Decoder 上交替使用。只要调用发生在合法的值位置,Decode(&item) 会从当前数组元素开始消费一个完整 JSON 值;下一轮 More() 会自然落到下一个元素或数组结尾。
未知字段要完整跳过,不能只读一个 Token
第一次写这类代码时,很容易在 default 分支里只调用一次 dec.Token()。如果未知值是字符串,这看起来有效;如果未知值是对象,一次调用只会读走开头的 {,后面的字段会被误当成顶层字段,解析位置随即错乱。
skipValue 需要先消费值的第一个 token。遇到标量时已经完成;遇到对象或数组时则递归消费其中的每个值,最后再读取对应闭合分隔符。
func skipValue(dec *json.Decoder) error {
// 先读取未知值的第一个 Token,标量到这里就已消费完。
tok, err := dec.Token()
if err != nil {
return err
}
delim, isContainer := tok.(json.Delim)
if !isContainer {
return nil
}
switch delim {
case '{':
for dec.More() {
// 对象成员先消费键,再递归跳过对应的值。
key, err := dec.Token()
if err != nil {
return err
}
if _, ok := key.(string); !ok {
return fmt.Errorf("未知对象的键不是字符串: %v", key)
}
if err := skipValue(dec); err != nil {
return err
}
}
return expectDelim(dec, '}')
case '[':
for dec.More() {
// 数组成员可能继续嵌套对象或数组。
if err := skipValue(dec); err != nil {
return err
}
}
return expectDelim(dec, ']')
default:
return fmt.Errorf("值位置出现意外闭合分隔符 %q", delim)
}
}

数字、偏移和 Reader 缓冲是三个容易忽略的边界
数字精度:Token 返回的数字默认按 float64 处理。调用 UseNumber 后,解码到接口值的数字会保留为 json.Number,之后可以根据业务选择 Int64、Float64 或保留字符串。结构体字段如果有明确整数类型,也可以直接声明为 int64,让 Decode 执行类型检查。
错误位置:InputOffset 表示最近返回 token 结束处与下一个 token 开始处的字节偏移。它适合包装到错误中帮助定位,但不是行列号;需要行号时,可以在输入层额外维护换行索引,或保存错误附近的受限片段。
预读行为:NewDecoder 有自己的缓冲,可能从底层 io.Reader 读取超过当前 JSON 值的字节。如果 JSON 后面还拼接了自定义协议内容,不能假设底层 Reader 正好停在对象末尾;官方提供的 Buffered 可访问 Decoder 已读入但尚未被 Token 或 Decode 消费的数据,并且该 reader 只在下一次解码调用前有效。
什么时候我会选 Token,什么时候不会
如果输入只是普通配置文件,整体尺寸可控,直接 Decode 到明确结构体通常更简单,也更容易配合 DisallowUnknownFields 做严格字段检查。Token 更适合“大容器里只关注一部分字段”“巨大数组逐条处理”“协议需要在不同字段采用不同策略”这三类场景。
Token 也不是自动的性能捷径。它减少的是整份数据同时驻留的需求,但每次 Token、类型断言、错误包装和单元素 Decode 仍有成本。若单个 item 本身就是几十兆字节,逐 item Decode 依然会占用相应内存;这时需要继续把 item 内部拆成 Token 层级,或者从数据生产端改成 NDJSON、分块记录等更适合流处理的格式。
| 场景 | 建议 | 原因 |
|---|---|---|
| 小型固定结构 | 直接 Decode | 代码最短,结构约束清楚 |
| 大数组逐条入库 | Token + More + 单元素 Decode | 内存边界约为一个元素 |
| 只读取少数字段 | Token 导航并跳过其余值 | 避免构造无用对象 |
| 未知字段必须报错 | 结构体 Decode + DisallowUnknownFields | 严格模式比手工跳过更合适 |
| 连续独立 JSON 记录 | 循环 Decode 或 NDJSON | 不必手工遍历每个 token |
常见问题
More 可以判断输入流是否结束吗?
不可以。More 只回答当前对象或数组里是否还有元素。顶层结束应通过消费闭合分隔符,并按协议检查后续是 io.EOF、下一个独立 JSON 值,还是其他数据。
Token 会自动把对象键和值配对吗?
不会。对象内部的调用顺序是字段名 token、字段值 token,如此重复。代码需要知道当前处于对象键位置还是值位置,最简单的办法就是在 for dec.More() 中先读键,再立即消费一个完整值。
可以在数组循环里直接调用 Decode 吗?
可以。先用 Token 消费数组开头的 [,然后在 More() 循环内对当前元素调用 Decode,循环结束后再消费 ]。
为什么未知字段不能简单调用一次 Token?
一次 Token 只能完整消费一个标量,遇到对象或数组只会读到开头分隔符。必须递归读取内部成员直到匹配的闭合分隔符,否则后续解析层级会错位。
Lanerc动漫网页版GPU占用高怎么办?硬件加速与浏览器排查说明
- 上一篇
- Lanerc动漫网页版GPU占用高怎么办?硬件加速与浏览器排查说明
- 下一篇
- GitHub Desktop 按提交恢复单个文件的操作
-
- Golang · Go教程 | 33分钟前 |
- Go encoding/xml Token 流式读取大型 XML
- 424浏览 收藏
-
- Golang · Go教程 | 52分钟前 | go · encoding/json · JSON Go encoding/json omitempty
- Go encoding/json omitempty 对零值字段的输出边界
- 192浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go encoding/json Decoder UseNumber 保留大整数精度
- 328浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go fmt.Scanner 自定义扫描规则的实现要点
- 182浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go fmt.Appendf 追加格式化结果的低分配写法
- 478浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go strings.IndexByte 定位协议分隔符的低分配写法
- 413浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · Strings · Go 字符串前缀 strings.CutPrefix
- Go strings.CutPrefix 处理可选前缀的分支设计
- 165浏览 收藏
-
- Golang · Go教程 | 4小时前 |
- Go bytes.Buffer Grow 预分配容量的估算方法
- 488浏览 收藏
-
- Golang · Go教程 | 4小时前 | Go教程 · Go 协议解析 bytes.CutPrefix 二进制帧
- Go bytes.CutPrefix 解析带标记的二进制帧
- 433浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 258次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 302次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 282次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 259次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 68次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

