当前位置:首页 > 文章列表 > Golang > Go问答 > Go json.Decoder.Token 读取混合 JSON 流时怎么定位对象边界

Go json.Decoder.Token 读取混合 JSON 流时怎么定位对象边界

来源:17golang原创 2026-09-09 19:37:31 0浏览 收藏

如果输入是 {"id":1}{"id":2}[{"id":3}] 这样的连续 JSON 流,json.Decoder.Token() 可以帮助你识别分隔符,但它不会直接返回“这个对象从第几字节到第几字节”。实用做法是:把 {[ 视为一层结构,遇到对应的关闭分隔符就减一层;当最外层深度回到 0,当前顶层值才算结束。

如果目标是拿到对象字段,而不是只标记边界,优先使用 Decode 循环。Token 适合结构探测和偏移记录,Decode 适合把一个完整顶层值交给 RawMessage 或结构体。两者不要在同一个解码器上无意识地交替,否则前者已经消费的 token 无法回退。

要点速览
  • 对象边界不是看到任意一个 } 就结束,而是最外层深度从 1 回到 0。
  • Token 返回分隔符和基础值,InputOffset 可记录当前 token 结束、下一个 token 开始的位置。
  • 需要保留完整 JSON 内容时,用 Decode(&raw) 一次取一个顶层值更稳妥。

先把“对象结束”定义成顶层深度回到 0

Go json.Decoder.Token 在混合 JSON 流中用顶层深度区分对象和嵌套数组边界
图1:外层对象的结束位置由深度回到 0 决定,内层数组的关闭分隔符不能提前结束对象。

Token 返回的内容主要有两类:json.Delim 表示 { } [ ],其他 token 表示字符串、数字、布尔值或 null。字段名本身也是字符串 token,所以不能用“读到字符串后下一个 token 就是字段值”来判断对象是否结束,真正可靠的信号是分隔符的嵌套层级。

token结构含义边界动作
{[进入对象或数组深度加一
}]离开当前对象或数组深度减一
字符串、数字、布尔值、null普通 JSON 值不改变深度

例如 {"meta":{"tags":["go"]},"id":7} 中,tags 数组结束只让深度回到对象内部;最后一个 } 才把对象深度降到 0。标准库同时保证返回的分隔符正确嵌套,遇到异常分隔符会返回错误,因此应用层主要负责判断根值类型和记录错误位置。

用 Token 扫描连续顶层值并记录字节区间

Go Decoder InputOffset 记录连续 JSON 顶层对象开始和结束字节位置的静态结构图
图2:InputOffset 把前一个 token 的结束位置与下一个 token 的起始位置连接起来,可用于记录顶层值区间。

下面的扫描器不尝试重建 JSON 文本,只负责确认一个顶层值何时结束,并输出它在输入流中的偏移范围。调用 InputOffset 前先读取当前位置作为起点,消费完根值后再读取一次作为终点;空白会被解码器跳过,偏移表示字节位置,不是中文字符数。

package main

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

// scanTopValue 消费一个顶层 JSON 值,并返回它的字节边界。
func scanTopValue(dec *json.Decoder) (int64, int64, error) {
    start := dec.InputOffset() // 这里是下一个 token 的起始位置。
    token, err := dec.Token()
    if err != nil {
        return 0, 0, err
    }

    delim, isDelim := token.(json.Delim)
    if !isDelim {
        // 字符串、数字、布尔值和 null 本身就是完整的顶层值。
        return start, dec.InputOffset(), nil
    }
    if delim != '{' && delim != '[' {
        return 0, 0, fmt.Errorf("顶层值不能以 %q 开始", delim)
    }

    depth := 1 // 根对象或数组已经打开,嵌套结构从这一层开始计算。
    for depth > 0 {
        token, err = dec.Token()
        if err != nil {
            return 0, 0, err // EOF 或语法错误都要保留给调用方处理。
        }
        if d, ok := token.(json.Delim); ok {
            switch d {
            case '{', '[':
                depth++ // 进入内层对象或数组。
            case '}', ']':
                depth-- // 离开一层;回到零才是根值结束。
            }
        }
    }
    return start, dec.InputOffset(), nil
}

func main() {
    input := `{"id":1,"meta":{"ok":true}} {"id":2} [1,2]`
    dec := json.NewDecoder(strings.NewReader(input))
    for {
        start, end, err := scanTopValue(dec)
        if err == io.EOF {
            break // 没有下一个顶层值,正常结束扫描。
        }
        if err != nil {
            panic(err) // 示例直接终止;服务代码应记录偏移和原始错误。
        }
        fmt.Printf("顶层值范围: %d-%d\n", start, end)
    }
}

这个例子能回答“边界在哪里”,但不能让你在扫描后再调用 Decode 读取同一个对象,因为 token 已经被消费。若只需要统计对象数量、记录日志位置、判断根值类型,这种方式足够;若还要读取字段,应直接走下面的完整值解码路径。

需要字段内容时用 Decode 消费完整值

Decoder.Decode 每次读取一个完整的 JSON 值,所以连续对象和对象后跟数组都可以用同一个循环处理。先接收为 json.RawMessage,可以把“边界确认”和“业务结构选择”分开;确认根值确实是对象后,再反序列化到目标类型。

package main

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

type Event struct {
    ID int `json:"id"`
}

func main() {
    input := `{"id":1}{"id":2}`
    dec := json.NewDecoder(strings.NewReader(input))
    dec.UseNumber() // 保留数字字面量,避免先变成 float64 再判断。

    for {
        var raw json.RawMessage
        if err := dec.Decode(&raw); err == io.EOF {
            break // 所有顶层值都已消费。
        } else if err != nil {
            return // 生产代码应结合 dec.InputOffset 记录失败位置。
        }

        var event Event
        if err := json.Unmarshal(raw, &event); err != nil {
            return // 根值不是预期对象时,在业务层拒绝它。
        }
        fmt.Println(event.ID)
    }
}

如果输入本来就是一个数组,可先用 Token 读出 [,再在 dec.More() 为真时调用 Decode,最后读取 ]More 只表示当前对象或数组里是否还有元素,不能拿它判断多个顶层对象是否还有下一个;顶层循环应以 Decodeio.EOF 为结束条件。

边界、错误和缓冲区怎么复查

生产代码中,先决定你要的是“结构位置”还是“业务对象”。前者可以使用 Token 和偏移;后者直接 Decode,避免自己维护一套 JSON 重建器。两者都要对错误做分层记录:语法错误说明输入结构坏了,字段反序列化错误说明结构合法但不符合业务类型。

  • 不要把 InputOffset 当字符下标。 它是输入流字节偏移,中文和 ASCII 的字节长度不同。
  • 不要用 More 扫描顶层流。 它只对当前对象或数组有效,连续顶层值应看 Decodeio.EOF
  • 不要假设 Decoder 没有预读。 标准库允许它从底层 Reader 多读一些数据;若要接管剩余缓冲,需理解 Buffered() 的生命周期。
  • 面对超大对象要设置资源边界。 Token 也会持续读取输入,业务层应限制单条值大小、记录异常偏移,并决定是否丢弃连接。

常见问题

Token 能直接返回对象的原始 JSON 字节吗?

不能。它返回 token,不提供当前对象的原始切片。要保留完整值,使用 Decode(&raw);要做字节定位,可结合 InputOffset 和你自己的受控缓冲区。

嵌套数组的 ] 到来时为什么不能算对象结束?

因为它只关闭数组这一层。只有根对象的 } 让深度从 1 降到 0,才说明对象整体结束。

连续 JSON 对象之间必须有换行吗?

不必须。Decoder 读取的是连续顶层 JSON 值,空白包括换行只是可选分隔;没有空白的 {"id":1}{"id":2} 也可以逐个 Decode。

怎样判断扫描出来的区间是否可靠?

让扫描器遇到 EOF、非法分隔符和未闭合结构时都返回错误,并把 InputOffset 写入日志。不要只根据计数器成功归零就忽略 token 错误。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go math/big.Float 设置精度后为什么结果仍会舍入Go math/big.Float 设置精度后为什么结果仍会舍入
上一篇
Go math/big.Float 设置精度后为什么结果仍会舍入
pkg.go.dev API 发布后如何程序化获取模块信息
下一篇
pkg.go.dev API 发布后如何程序化获取模块信息
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    51次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    201次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    137次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    68次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    49次使用