登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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

来源:17golang原创

时间:2026-09-09 19:37:31 240浏览 收藏

如果输入是 {"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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>