Go jsontext 怎样读取 JSON token 而不先构造对象
来源:17golang原创
时间:2026-10-25 19:40:42 342浏览 收藏
我第一次把大 JSON 输入接到筛选逻辑时,最容易想到的是先解码成 map[string]any,再从对象里找字段。这样写很快,但如果只想检查几个字段,或者希望把未命中的嵌套内容原样转交,先构造完整对象就显得太重了。encoding/json/jsontext 提供了另一种粒度:让 Decoder 按顺序读出 JSON token,调用方只在需要时消费一段完整 value。
官方地址:https://pkg.go.dev/encoding/json/jsontext
直接回答:用jsontext.NewDecoder(reader)创建解码器,在循环中调用ReadToken()读取结构符号、字段名和标量值;读到io.EOF代表输入结束。它处理的是 JSON 语法流,不会自动把 token 变成你的业务 struct。
这套 API 对我最有价值的地方,不是“少写一个结构体”,而是可以把读取范围缩小到真正需要判断的位置。下面按一次实际的流式筛选思路展开:先认识 token,再处理嵌套结构,最后决定哪些内容需要读成完整 value。
先把 jsontext 放在语法层理解
jsontext 的核心对象是 Token 和 Value。Token 表示 JSON 的基本结构单元,例如对象或数组的开始、结束、字符串、数字和布尔值;Value 则表示一个完整的 JSON 值,可以是标量,也可以是整个对象或数组。两者都能表示字符串和数字,但只有 Value 能把一个嵌套对象整体保留下来。
所以它和“把 JSON 映射到 Go 类型”的工作不是一层。语法层适合做顺序扫描、结构判断、保留原文或跳过不关心的部分;语义层才适合把确定的字段解释成业务对象。把这两个目标混在一起,往往会让读取循环既难维护,又无法控制内存范围。
| 读取对象 | 适合处理什么 | 调用结果 |
|---|---|---|
ReadToken | 结构符号、字段名、标量值、顺序扫描 | 返回一个 Token 和错误 |
ReadValue | 完整对象、完整数组、需要保留原文的片段 | 返回一段原始 JSON Value |
PeekKind | 读取前判断下一个 token 的种类 | 返回 Kind,不消费输入 |

用 ReadToken 顺序读取一份 JSON
最小可用循环只需要一个 io.Reader、一个 Decoder 和 ReadToken。判断字符串 token 时可以使用 Kind(),再用 String() 取得字符串内容。数值 token 不要一律当成字符串;如果业务需要整数,可以调用 Int() 或 Uint() 并处理转换错误。
package main
import (
"fmt"
"io"
"strings"
"encoding/json/jsontext"
)
func main() {
input := strings.NewReader(`{"name":"Ada","active":true,"scores":[10,12]}`)
dec := jsontext.NewDecoder(input)
for {
// 每次只消费一个 JSON token,不先创建 map 或业务 struct。
tok, err := dec.ReadToken()
if err == io.EOF {
// EOF 表示 token 流正常结束,不是业务错误。
break
}
if err != nil {
// 语法错误等非 EOF 错误需要带着当前位置返回或记录。
panic(err)
}
// Kind 用于判断 token 类型,String 只适合读取字符串 token 的内容。
fmt.Printf("kind=%s token=%q\\n", tok.Kind(), tok.String())
}
}
这段代码的重点是错误分支:只有 io.EOF 表示输入读完,其他错误说明当前 JSON 或读取状态有问题。实际服务中不建议直接 panic,可以把 dec.InputOffset() 一起写入日志,让调用方知道错误大致位于输入的哪个字节偏移。
用 PeekKind 和栈信息缩小扫描范围
顺序读取不等于盲目读取。调用 PeekKind() 可以先观察下一个 token 的类型而不消费它;对象或数组的嵌套关系则可以通过 StackDepth()、StackIndex() 和 StackPointer() 了解。这样做适合写“只关注当前对象的某个字段”的扫描器,但不要把栈信息当成业务路径本身,业务路径仍应由字段名和结构状态共同决定。
func inspectNext(dec *jsontext.Decoder) error {
// PeekKind 只查看下一个 token,后续仍需 ReadToken 才会前进。
kind := dec.PeekKind()
if kind == '}' || kind == ']' {
// 结束分隔符说明当前嵌套结构即将闭合。
return nil
}
// 输入偏移量和栈深度一起记录,便于定位大 JSON 中的当前层级。
fmt.Printf("offset=%d depth=%d next=%s\\n", dec.InputOffset(), dec.StackDepth(), kind)
return nil
}
如果只需要看一层对象,通常的实现方式是维护“当前是否刚读到字段名”的小状态,然后按字段名决定下一步读 token、读完整 value,或跳过值。不要依赖 token 的打印结果来判断结构,因为对象成员之间的冒号和逗号并没有独立的 Token 表示,它们由 JSON 语法状态推断出来。
什么时候应该改用 ReadValue
token 流适合检查结构,但遇到嵌套对象或数组时,我通常会在“需要原样保留”与“需要继续拆解”之间做明确选择。如果某个字段的值要交给后续模块延迟解析,直接调用 ReadValue() 能保留这一完整值;如果只想继续扫描内部字段,则继续用 ReadToken(),但要确保调用方理解对象和数组的开始、结束边界。
func readOneValue(dec *jsontext.Decoder) (jsontext.Value, error) {
// ReadValue 会消费一个完整 JSON 值,包括嵌套对象或数组。
value, err := dec.ReadValue()
if err != nil {
// 返回原始错误,调用方可结合 InputOffset 做上下文记录。
return nil, err
}
// Value 保留原始 JSON 字节,适合交给稍后的语义解析步骤。
return value, nil
}

两种方法不能随意交替。每次调用都会推进同一个解码器状态机:如果已经用 ReadToken 消费了对象开始符号,再用 ReadValue,它读取的范围就取决于当前游标;如果当前正位于字段名之后,它消费的是该字段对应的下一个完整值。设计读取器时,最好把“谁消费哪个边界”写在函数名和注释里。
字段筛选时如何避免把对象先全部展开
一个实用模式是:遇到对象开始后,读取字段名;对需要判断的字段读一个 token 或 value;对不关心的字段调用 SkipValue()。这样可以把扫描成本集中到目标字段,同时保持解码器继续处于合法的 JSON 状态。
func scanObject(dec *jsontext.Decoder) (string, error) {
var target string
for {
tok, err := dec.ReadToken()
if err != nil {
return target, err
}
if tok.Kind() == '}' {
// 对象结束后返回已经筛选到的字段。
return target, nil
}
// 对象成员名是字符串 token,读取后决定值的消费方式。
if tok.Kind() != '"' {
return target, fmt.Errorf("unexpected member token: %s", tok.Kind())
}
name := tok.String()
if name == "target" {
value, err := dec.ReadValue()
if err != nil {
return target, err
}
// 只在目标字段上保留原始值,避免创建无关对象。
target = value.String()
} else if err := dec.SkipValue(); err != nil {
// SkipValue 负责消费当前字段对应的完整值及其嵌套内容。
return target, err
}
}
}
上面的循环展示的是读取职责,而不是完整的业务反序列化器。生产代码还应明确对象开始符号、字段名和值之间的状态,并在遇到错误时保留输入偏移。字段值如果要保持原始字节,最好直接保存 Value 或其克隆结果,避免后续缓冲区生命周期与调用顺序互相影响。
错误、EOF 和输入边界要分开处理
在流式读取里,io.EOF 只说明整个 reader 已经没有更多 token;它和“JSON 结构没有闭合”的语法错误不是一回事。错误处理建议分三层:正常结束直接退出;语法或状态错误记录偏移并返回;字段没有命中则交给业务决定是否继续或返回默认值。
| 情况 | 处理方式 | 调用方要记住什么 |
|---|---|---|
读到 io.EOF | 结束 token 循环 | 这是正常输入结束,不等于命中目标字段 |
| 语法或状态错误 | 返回错误并记录 InputOffset | 不要把错误当作空对象或默认值 |
| 字段未命中 | 继续扫描、跳过或返回业务错误 | 这是业务结果,不一定是 JSON 错误 |
| 需要嵌套原文 | 用 ReadValue 或先复制内容 | 明确谁拥有这段原始 JSON |
我会怎样选择读取粒度
如果目标是统计字段、检查标志位或判断某个成员是否存在,我会优先使用 ReadToken 配合 SkipValue,因为扫描的状态和内存范围都比较清楚。如果目标是把一个未知结构交给另一个模块,我会在字段值入口使用 ReadValue,把语法读取和后续语义解析拆开。只有需要完整业务对象时,才进入更高层的反序列化流程。
这套选择也解释了标题里的“不先构造对象”:它不是说 JSON 永远不需要对象,而是把对象构造推迟到真正需要的边界。jsontext 适合做语法流的前置筛选、转换和保留;它不会替你完成字段类型校验、业务默认值或领域对象创建。
ReadToken 会把逗号和冒号读出来吗?不会。对象成员之间的冒号、成员之间的逗号可以由 JSON 结构推断,因此没有独立的 Token 表示。
ReadValue 返回的是业务对象吗?不是。它返回的是完整 JSON 值的原始表示,后续是否映射为 struct 由调用方决定。
为什么不能把 EOF 当成字段缺失?EOF 只代表输入结束;字段缺失是业务层判断,二者需要分别记录和处理。
我最后保留的经验是:先定义“这一步要消费多大的 JSON 范围”,再选择 ReadToken、ReadValue 或 SkipValue。范围明确后,流式读取的代码会比先展开成通用对象更容易控制,也更容易把错误位置和字段筛选结果解释清楚。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
175 收藏
-
317 收藏
-
176 收藏
-
159 收藏
-
245 收藏
-
422 收藏
-
194 收藏
-
198 收藏
-
229 收藏
-
306 收藏
-
471 收藏
-
245 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习