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

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

来源:17golang原创

时间:2026-10-28 18:45:51 427浏览 收藏

当 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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>