当前位置:首页 > 文章列表 > Golang > Go教程 > Go jsontext 怎样读取 JSON token 而不先构造对象

Go jsontext 怎样读取 JSON token 而不先构造对象

来源:17golang原创 2026-10-25 19:40:42 0浏览 收藏

我第一次把大 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,不消费输入
Go jsontext Decoder 把 JSON 输入转换为连续 token 流而不先构造 Go 对象的结构说明图
图1:JSON 输入经过 Decoder 变成 token 流的静态结构说明图,不是浏览器、终端或运行截图。

用 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
}
Go jsontext 中 ReadToken 与 ReadValue 的读取粒度和完整值边界对比说明图
图2:ReadToken 与 ReadValue 的读取粒度、嵌套边界和语法层职责说明图,不是运行证据。

两种方法不能随意交替。每次调用都会推进同一个解码器状态机:如果已经用 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。范围明确后,流式读取的代码会比先展开成通用对象更容易控制,也更容易把错误位置和字段筛选结果解释清楚。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go encoding/json/v2 怎样从 Reader 解码多个值Go encoding/json/v2 怎样从 Reader 解码多个值
上一篇
Go encoding/json/v2 怎样从 Reader 解码多个值
Go crypto/mldsa NewPrivateKey 从32字节种子恢复私钥
下一篇
Go crypto/mldsa NewPrivateKey 从32字节种子恢复私钥
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    689次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    702次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    762次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    727次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    561次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码