当前位置:首页 > 文章列表 > Golang > Go教程 > Go json.Decoder.Token 怎样按顺序读取 JSON 标记

Go json.Decoder.Token 怎样按顺序读取 JSON 标记

来源:17golang原创 2026-10-28 18:45:51 0浏览 收藏

当 JSON 数据来自文件、网络连接或一串连续的顶层值时,直接把全部内容反序列化成一个大结构并不总是合适。json.Decoder.Token 会从输入流中按顺序取出下一个 JSON 标记:对象和数组的分隔符以 json.Delim 返回,布尔值、字符串、数字和 null 返回各自的基础类型,读完输入后返回 io.EOF。

官方文档:https://pkg.go.dev/encoding/json#Decoder.Token

把 Token 当成“JSON 词法游标”使用:需要知道结构边界时逐个读取,需要把某个完整值映射到 Go 类型时再交给 Decode。Token 不会替你把对象键和值组成业务结构。

Token 返回的不是一组统一结构

Token 的返回类型是接口,调用方应先处理错误,再根据动态类型判断当前标记。四个括号分隔符会以 json.Delim 出现;对象的键仍然是普通字符串,数字默认是 float64,除非通过其他方式保留数字文本。

json.Decoder.Token 返回值类型与 JSON 分隔符的静态关系说明图
图1:Token 返回值关系说明图,展示分隔符、键、基础值和 EOF 的边界;这是静态说明图,不是运行截图。
package main

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

func main() {
    dec := json.NewDecoder(strings.NewReader(`{"name":"Go","ok":true,"count":2}`))
    for {
        tok, err := dec.Token()
        if err == io.EOF { // 正常读完当前 JSON 流,退出循环。
            break
        }
        if err != nil { // 语法错误或输入读取错误必须保留,不能当成 EOF。
            panic(err)
        }
        switch value := tok.(type) {
        case json.Delim: // [、]、{、} 用于判断数组和对象边界。
            fmt.Printf("分隔符: %q\\n", value)
        default: // 键、字符串、布尔值、数字和 null 从这里进入。
            fmt.Printf("值: %T %v\\n", value, value)
        }
    }
}

这段代码的关键是不要把“读完一个值”和“读完整个流”混为一谈。一个顶层对象读完后,如果输入后面还有另一个顶层 JSON 值,下一次调用仍可能得到新的标记。

用分隔符维护对象和数组的层级

Token 保证返回的 [ ] { } 是正确嵌套并匹配的,但它只负责给出标记,不会替你维护业务层级。需要统计当前深度、识别对象是否关闭或记录数组位置时,可以在读到左分隔符时入栈,在读到右分隔符时出栈。

func scanDepth(dec *json.Decoder) error {
    depth := 0
    for {
        tok, err := dec.Token()
        if err == io.EOF { // 流结束是正常结果,交给调用方决定是否接受。
            return nil
        }
        if err != nil { // Token 已经发现非法 JSON 或底层读取失败。
            return err
        }
        if delim, ok := tok.(json.Delim); ok {
            switch delim {
            case '{', '[':
                depth++ // 左分隔符进入一层对象或数组。
            case '}', ']':
                depth-- // 右分隔符离开一层;匹配由 Decoder 保证。
            }
            fmt.Printf("depth=%d token=%q\\n", depth, delim)
        }
    }
}

如果还要知道对象键对应的值,不能只看字符串类型,因为键和值都可能是字符串。应在对象上下文中交替维护“期待键”与“期待值”的状态,或者在确定目标字段后把该字段的值交给 Decode。

数组遍历时让 Token 和 Decode 各做擅长的事

只扫描结构时可以一直调用 Token;但当数组元素需要进入结构体时,最清晰的写法是先用 Token 读出数组开始分隔符,再用 More 判断是否还有元素,最后用 Decode 读取完整元素。

Token、More 与 Decode 在数组读取中的静态关系说明图
图2:数组读取边界说明图,展示 Token 负责边界、More 负责元素判断、Decode 负责值映射;这是结构说明图,不是运行证据。
type Item struct {
    Name string `json:"name"`
    ID   int    `json:"id"`
}

func readItems(dec *json.Decoder) ([]Item, error) {
    first, err := dec.Token()
    if err != nil { // 先确认输入确实以数组开始。
        return nil, err
    }
    if first != json.Delim('[') { // 业务需要数组时,拒绝其他顶层值。
        return nil, fmt.Errorf("want array, got %v", first)
    }

    var items []Item
    for dec.More() { // More 只用于当前数组或对象内部,不用于任意流。
        var item Item
        if err := dec.Decode(&item); err != nil { // 完整元素交给结构体解码。
            return nil, err
        }
        items = append(items, item)
    }
    end, err := dec.Token()
    if err != nil { // 读取数组结束符时仍要检查输入错误。
        return nil, err
    }
    if end != json.Delim(']') {
        return nil, fmt.Errorf("want array end, got %v", end)
    }
    return items, nil
}

这个组合适合“外层结构需要检查,元素内容需要映射”的场景。它避免把整个输入一次性读进内存,也让数组边界和元素错误的位置更容易定位。

EOF、截断和非法分隔符要分开处理

在循环中,只有 errors.Is(err, io.EOF) 表示输入流自然结束。JSON 在字符串、数字或字面量中途被截断时,通常应作为错误返回;遇到不匹配的分隔符也会得到错误。不要用“任意错误都等于读完”来结束业务处理,否则坏数据会被当成成功。

func readStream(dec *json.Decoder) error {
    for {
        tok, err := dec.Token()
        if err != nil {
            if errors.Is(err, io.EOF) { // 只有自然结束才是成功退出。
                return nil
            }
            return fmt.Errorf("read JSON token: %w", err) // 保留原始错误供上层判断。
        }
        fmt.Printf("token=%T %v\\n", tok, tok)
    }
}

使用这段代码时别忘了导入 errors、fmt、io。如果输入可能由多个顶层 JSON 值组成,应把每一轮 Token 读取视为同一条流的连续消费,而不是假定只有一个对象。

常见问题

Token 能直接拿到对象字段名吗?能拿到字段名,但它只返回字符串,调用方仍要根据当前对象状态判断这个字符串是键还是值。

什么时候不该用 Token?如果目标是把完整 JSON 映射为稳定的结构体,并不需要检查中间层级,直接使用 Decode 通常更简单;Token 更适合流式扫描、结构探测和局部读取。

小结:先用 Token 看清 JSON 的顺序和边界,再用 More 管数组或对象内部循环,最后把完整业务值交给 Decode。正常 EOF、输入截断和非法结构必须分别处理。

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