当前位置:首页 > 文章列表 > Golang > Go教程 > jsontext按Token读取超大JSON

jsontext按Token读取超大JSON

来源:17golang原创 2026-11-15 03:08:10 0浏览 收藏

处理一个几 GB 的 JSON 文件时,最容易踩的坑不是 JSON 语法,而是把整份文档一次性反序列化成 Go 结构体。Go 1.27 提供的 encoding/json/jsontext 更接近 JSON 的语法层:用 Decoder.ReadToken 逐个读取结构符号、字段名、字符串、数字和字面量,业务代码只保留当前需要的状态。

官方地址:https://pkg.go.dev/encoding/json/jsontext

如果目标是扫描超大 JSON 中的少数字段,优先把输入接到 jsontext.NewDecoder,循环调用 ReadToken,把 io.EOF 当作正常结束;不要把每个 Token 追加到切片。需要跨下一次读取保留原始 Token 时,再调用 Clone。

先确定:你要 Token 还是完整 Value

jsontext 同时提供 ReadToken 和 ReadValue。前者读取一个词法 Token,例如 {、字段名、数字或 ];后者读取一个完整 JSON 值,完整对象和数组也属于 Value。两条路线都能流式推进,但内存边界和处理粒度不同。

需求优先选择原因
扫描字段名、统计结构、过滤少数成员ReadToken每次只处理当前 Token,状态可由业务代码控制
把一个完整对象交给下游组件ReadValue一次得到完整 JSON 值,调用方不必自己拼接边界
保存从解码器读出的 TokenToken.Clone原始 Token 的数据只保证在下一次 Decoder 调用前有效
jsontext 从 io.Reader 流式读取超大 JSON 并按 Token 输出的结构示意图
图1:jsontext 按 Token 读取超大 JSON 的静态结构示意图,不是运行截图。

从任意 io.Reader 创建流式 Decoder

文件、HTTP 响应体和网络连接都可以实现 io.Reader,所以扫描函数不必绑定具体输入来源。下面的函数只接收 Reader,并把每个 Token 的原始显示形式交给回调;它不会把整份 JSON 放进 []byte 或结构体。

package main

import (
    "encoding/json/jsontext"
    "fmt"
    "io"
    "strings"
)

// scanTokens 逐个读取 JSON Token,调用方决定如何消费当前 Token。
func scanTokens(r io.Reader, visit func(jsontext.Token, int) error) error {
    dec := jsontext.NewDecoder(r)

    for {
        tok, err := dec.ReadToken()
        if err == io.EOF {
            // 输入流正常结束,不把 EOF 当成失败。
            return nil
        }
        if err != nil {
            // 语法错误或底层 Reader 错误都交给上层处理。
            return err
        }

        // StackDepth 表示当前处于多少层对象或数组嵌套。
        if err := visit(tok, dec.StackDepth()); err != nil {
            return err
        }
    }
}

func main() {
    err := scanTokens(strings.NewReader(`{"items":[{"id":1},{"id":2}]}`), func(tok jsontext.Token, depth int) error {
        // String 对字符串返回解码后的值,对其他 Token 返回原始表示。
        fmt.Printf("depth=%d token=%s\n", depth, tok.String())
        return nil
    })
    if err != nil {
        fmt.Println("scan failed:", err)
    }
}

上面示例的 main 需要额外导入 strings 才能作为完整演示;生产代码通常把文件或 HTTP Body 直接传给 scanTokens。示例故意只打印当前 Token,不把 Token 收集到切片,这才是大文档扫描时的关键。

循环处理时,EOF 和错误要分开

ReadToken 返回 io.EOF 代表没有更多 Token,是成功路径;其他错误则可能表示 JSON 语法不完整、输入截断或底层 Reader 失败。把所有错误都当作 EOF,会让损坏的文件看起来像是成功处理。

func countNumbers(r io.Reader) (int, error) {
    dec := jsontext.NewDecoder(r)
    count := 0

    for {
        tok, err := dec.ReadToken()
        if err == io.EOF {
            // 全部 Token 已读完,返回累计结果。
            return count, nil
        }
        if err != nil {
            // 不吞掉语法错误,避免把半份数据当成完整结果。
            return 0, fmt.Errorf("read JSON token: %w", err)
        }

        // Kind 可用于区分字符串、数字和对象/数组边界。
        if tok.Kind() == jsontext.KindNumber {
            count++
        }
    }
}

这里的计数条件只用于展示处理位置,实际项目应按当前 Go 版本的 jsontext.Kind 常量表达数字类型,不要依赖字符字面量猜测 Token 类型。更常见的做法是先根据字段名维护一个小状态机,再对目标值调用 tok.Int()、tok.Uint() 或 tok.Float(),并检查转换错误。

用字段状态和嵌套深度筛选目标数据

按 Token 读取并不意味着业务逻辑必须复杂。面对类似 {"items":[...]} 的文档,可以只保留“最近读到的字段名”“是否正在目标数组内”“当前深度”几个变量;读到目标值后立即处理,读到结束分隔符时清除状态。

func scanItemIDs(r io.Reader, handle func(string) error) error {
    dec := jsontext.NewDecoder(r)
    wantID := false

    for {
        tok, err := dec.ReadToken()
        if err == io.EOF {
            // 没有更多 Token,扫描成功结束。
            return nil
        }
        if err != nil {
            // 将 JSON 语法错误原样包装后返回。
            return fmt.Errorf("scan item ids: %w", err)
        }

        if tok.Kind() == jsontext.KindString {
            text := tok.String()
            if text == "id" {
                // 下一个 Token 是字段值,交给后续分支处理。
                wantID = true
                continue
            }
        }
        if wantID {
            // 这里示意把当前值交给业务层;真实代码应按 Kind 转换数值。
            if err := handle(tok.String()); err != nil {
                return err
            }
            wantID = false
        }
    }
}

生产代码还要把字段状态和对象层级绑定起来,避免嵌套对象里同名的 id 被误处理。StackDepth 能帮助你记录当前嵌套深度;如果需要精确判断当前路径,应维护一个与对象/数组边界同步的小型路径栈,而不是依赖“最近一次字段名”。

ReadToken 与 ReadValue 的边界

ReadToken 适合“看见一个就处理一个”,但它不会代表完整对象或数组;ReadValue 会返回一个完整的 JSON Value,适合把单个成员交给另一个解析器。选择时要看下游需要的最小单位,而不是简单追求 API 数量更少。

jsontext ReadToken 与 ReadValue 的选择边界和 Token Clone 生命周期示意图
图2:ReadToken 与 ReadValue 的静态选择示意图,底部生命周期提示对应官方 API 约定。

另一个容易被忽略的边界是 Token 的生命周期。ReadToken 返回的原始 Token 可能由 Decoder 的内部缓冲区支持;下一次 PeekKind、ReadToken、ReadValue 或 SkipValue 调用后,不能继续把它当作长期对象使用。若要放进队列、异步任务或跨循环保存,先复制:

func keepFirstToken(r io.Reader) (jsontext.Token, error) {
    dec := jsontext.NewDecoder(r)
    tok, err := dec.ReadToken()
    if err != nil {
        return jsontext.Token{}, err
    }

    // Clone 让 Token 脱离 Decoder 的内部缓冲区,便于跨下一次读取保存。
    saved := tok.Clone()
    return saved, nil
}

大文档场景下的内存和失败边界

  • 文档总大小不等于单次内存:按 Token 扫描通常不需要保留整份文档,但单个超长字符串或数字仍可能成为最大瞬时 Token。
  • 不要无界缓存:如果要把结果交给下游,用有界队列、批量提交或背压,不要把所有 Token 再复制一份。
  • Reader 错误不能吞:网络中断、文件截断和语法错误应和正常 EOF 分开记录。
  • 路径状态要有边界:同名字段可能出现在不同层级,使用对象/数组边界更新路径栈。
  • 版本要对齐:encoding/json/jsontext 是 Go 标准库的语法层 API,部署环境应与文章示例所用 Go 版本一致。

小结:把“整份解码”改成“当前 Token 决策”

jsontext 按 Token 读取超大 JSON 的核心不是一段特殊的循环,而是重新划分责任:Decoder 负责语法推进,业务代码只保留字段、路径和结果所需的最小状态;io.EOF 是正常结束,其他错误必须向上返回;需要长期保存的 Token 用 Clone 脱离内部缓冲区。这样才能在输入从几十 MB 增长到 GB 级时,仍然让内存上限由“当前 Token 和业务窗口”决定,而不是由整个文档决定。

常见问题

ReadToken 会把整个 JSON 文件读进内存吗?

不会把完整文档一次性解码成结构体,但 Decoder 仍会缓冲并解析当前 Token;超长字符串本身仍可能占用较多内存。

什么时候应该改用 ReadValue?

当下游需要一个完整对象或数组,而不是逐个处理结构符号时,ReadValue 更直接;它的内存边界应按单个 Value 的大小评估。

为什么不能直接把 Token 存起来以后用?

由 Decoder 读取的原始 Token 只保证在下一次 Decoder 调用前有效。跨循环、异步或队列保存时调用 Clone。

发现 io.EOF 后还要再读一次吗?

不需要。对一次完整输入,io.EOF 表示 Token 流已经正常结束;重复读取没有意义,应该返回累计结果。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
jsonv2严格处理重复对象键jsonv2严格处理重复对象键
上一篇
jsonv2严格处理重复对象键
jsonv2为字段设置自定义格式
下一篇
jsonv2为字段设置自定义格式
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    1031次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1032次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1175次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1135次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    976次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码