Go 解析 JSON 流时 Token 边界错乱怎么办
在排查 Go 的流式 JSON 读取时,我遇到过一种很像“Token 越界”的现象:数组第一个元素读完后,下一次拿到的不是预期值;对象字段读取到一半时,Token() 又返回了 ] 或 }。这通常不是 encoding/json 把数据打乱了,而是把词法 token、容器分隔符和完整值混成了同一层。
处理这类问题的关键是固定职责:Token() 负责逐个读取 JSON token,json.Delim 负责标出数组或对象的起止边界,More() 只判断当前容器里是否还有元素,Decode() 则负责消费一个完整的值。只要围绕当前容器配对读取,所谓的“边界错乱”就会变成一个可以定位的读取位置问题。
先用Token()读取并确认容器起点,再在对应容器内部用More()判断是否还有值;数组元素或对象值交给Decode(),循环结束后再读取闭合分隔符。不要用一次Token()代替一次完整的业务对象读取。
先还原 Token 看似错位的现场
JSON 流里同时存在基本值和结构分隔符。字符串、数字、布尔值和 null 是基本值;[、]、{、} 是 json.Delim。逗号和冒号不会作为独立 token 返回,所以把返回序列直接打印出来时,看到的内容和原始 JSON 文本并不是一一对应的字符序列。
例如下面的数据包含一个对象和一个数组:
{"name":"go","ports":[80,443]}
按 token 观察时,可能依次看到对象起点、字段名、字段值、数组字段名、数组起点、两个数字、数组终点和对象终点。此时 ] 不是“多出来的数据”,而是告诉你数组已经结束;如果代码把它当成数组元素处理,后面的判断自然会错位。

根因通常是把 Token 当成了完整业务值
Token() 的返回类型是 json.Token,它描述的是当前位置的下一个 JSON token,而不是“下一个数组元素对应的 Go 结构体”。当数组元素本身是对象或数组时,一个元素会包含多个 token;如果每次只调用一次 Token() 就把结果强行转成业务字段,读取游标就会落在元素内部。
一个常见的误区是把 More() 放在任意读取循环里。它的语义是“当前正在解析的数组或对象里是否还有另一个元素”,不是“整个输入流是否还有 token”,也不是“上一次 Token 是否代表一个有效值”。离开容器后继续依赖 More(),就会把容器边界和流结束混在一起。
数组和对象的边界要先配对
修复时可以先建立一个小的边界纪律:读到 [ 后,只在这个数组内部调用 More();读到 { 后,只在这个对象内部调用 More();循环结束后再读取对应的 ] 或 }。闭合 token 能帮助你确认当前容器确实已经结束。
下面的辅助函数只负责判断 token 是否为指定的容器分隔符。判断失败时返回错误,调用方就不会在错误边界上继续消费输入。
package main
import (
"encoding/json"
"fmt"
)
// expectDelim 检查当前 token 是否是调用方期待的容器边界。
func expectDelim(token json.Token, want json.Delim) error {
delim, ok := token.(json.Delim)
if !ok || delim != want {
return fmt.Errorf("期待分隔符 %q,实际得到 %v", want, token)
}
return nil
}
这个检查不会改变读取位置,但能把“读到了不该出现的结束符”变成明确错误。它尤其适合嵌套数组或对象,因为嵌套层级一旦配错,错误通常会在后面才暴露。
数组元素交给 Decode 一次消费
如果数组元素是完整对象,就不要手工逐个取字段 token。先读取数组起点,再让 More() 判断是否仍有元素,每轮使用 Decode() 把一个对象消费到结构体中,最后读取数组终点。
package main
import (
"encoding/json"
"fmt"
"io"
"strings"
)
type Item struct {
Name string `json:"name"`
Port int `json:"port"`
}
// readItems 按数组边界读取完整对象,避免把对象内部 token 当成数组元素。
func readItems(dec *json.Decoder) ([]Item, error) {
start, err := dec.Token()
if err != nil {
return nil, err
}
if err := expectDelim(start, '['); err != nil {
return nil, err
}
items := make([]Item, 0, 4)
for dec.More() {
var item Item
// Decode 会消费当前数组中的一个完整 JSON 值。
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 err := expectDelim(end, ']'); err != nil {
return nil, err
}
return items, nil
}
func main() {
input := `[ {"name":"http","port":80}, {"name":"https","port":443} ]`
items, err := readItems(json.NewDecoder(strings.NewReader(input)))
if err != nil && err != io.EOF {
fmt.Println("读取失败:", err)
return
}
fmt.Println("读取到元素数:", len(items))
}
这里的分工很清楚:Token() 只读取起止边界,More() 只回答“容器里还有没有元素”,Decode() 负责把当前元素转换成 Item。如果数组元素改成嵌套对象,外层循环仍然不需要手工追踪内部字段。
对象字段要区分字段名和字段值
对象的 token 序列与数组不同:对象内部通常交替出现字段名和字段值。需要动态处理字段时,可以先读字段名,再根据字段名决定如何消费紧随其后的值;如果值的结构复杂,仍应交给 Decode() 或专门的递归函数。
package main
import (
"encoding/json"
"fmt"
"io"
)
// readObject 展示对象边界内的字段名和值如何配对读取。
func readObject(dec *json.Decoder) error {
start, err := dec.Token()
if err != nil {
return err
}
if err := expectDelim(start, '{'); err != nil {
return err
}
for dec.More() {
keyToken, err := dec.Token()
if err != nil {
return err
}
key, ok := keyToken.(string)
if !ok {
return fmt.Errorf("对象字段名不是字符串:%v", keyToken)
}
var value json.RawMessage
// RawMessage 让当前字段值整体落地,避免只消费值的第一个 token。
if err := dec.Decode(&value); err != nil {
return fmt.Errorf("字段 %q 解码失败:%w", key, err)
}
fmt.Printf("字段 %s 的 JSON 值长度:%d\n", key, len(value))
}
end, err := dec.Token()
if err != nil {
return err
}
if err := expectDelim(end, '}'); err != nil {
return err
}
return nil
}
var _ = io.EOF
对象循环里,Token() 读取的是字段名,字段值则由 Decode() 一次消费。这里使用 json.RawMessage 是为了保留值的完整 JSON 片段;实际项目也可以替换成具体结构体或 map[string]any。不能因为字段值恰好是字符串,就把所有字段都按字符串 token 读取。

用 InputOffset 把错位定位到输入位置
当输入来自网络或文件时,仅打印 token 内容往往不够。Decoder.InputOffset() 返回当前解码器位置的字节偏移:它指向最近一次返回 token 的结尾与下一个 token 的开头。可以把偏移写入错误日志,帮助判断问题发生在字段名、嵌套容器还是闭合分隔符附近。
func decodeWithOffset(dec *json.Decoder, dst any) error {
if err := dec.Decode(dst); err != nil {
// 偏移用于定位输入边界,不代表业务字段的字符下标。
return fmt.Errorf("JSON 解码失败,offset=%d:%w", dec.InputOffset(), err)
}
return nil
}
如果错误发生在 Token() 或 Decode() 之后,记录偏移和最近一次已识别的边界,比记录“第几个 token”更容易和原始输入对应。
这几种写法最容易再次触发边界问题
- 用
for dec.More()处理整个输入流,却没有先读入[或{。 - 读到数组起点后,每轮只调用一次
Token(),把对象起点、字段名或结束符误当成元素。 - 读完
More()循环后不读取闭合分隔符,直接开始解析外层下一个值。 - 把
More()当作 EOF 判断;流结束应该处理Token()或Decode()返回的io.EOF。 - 嵌套容器只维护一个“当前深度”变量,却没有让每层函数负责自己的起点和终点。
一张表记住四个方法的边界
| 方法或类型 | 它回答的问题 | 不要把它当成 |
|---|---|---|
Token() | 下一个 JSON token 是什么? | 一个完整业务对象 |
json.Delim | 数组或对象从哪里开始、结束? | 普通业务值 |
More() | 当前数组或对象里还有元素吗? | 整个输入流还有数据吗? |
Decode() | 如何消费下一个完整 JSON 值并映射到目标? | 只读取一个字段名 |
复盘结论
这类问题的影响通常不是 JSON 内容被修改,而是读取代码提前消费或错误解释了容器边界。排查时先打印并确认起止 Delim,再检查 More() 是否确实位于对应容器内部,最后确认每个业务元素是否由一次 Decode() 完整消费。
如果仍然出现错位,记录 InputOffset()、最近一次 token 类型和当前容器层级;这些信息足以把问题收敛到“字段名和值配对错误”“嵌套边界未闭合”或“把流结束误当成容器结束”中的一类。
相关问题
Token() 会返回逗号和冒号吗?
不会。逗号和冒号用于 JSON 语法分隔,不会作为独立 token 返回;数组和对象的起止符会以 json.Delim 返回。
More() 可以用来判断文件是否读完吗?
不可以。它只描述当前数组或对象的下一个元素是否存在。整个流是否结束应根据 Token() 或 Decode() 返回的 io.EOF 判断。
数组元素不是对象时还需要 Decode() 吗?
可以直接对数字、字符串等值使用 Decode(),也可以在确实需要逐 token 处理时使用 Token()。关键是先明确你处理的是完整值,还是故意处理词法 token。
资料出处:https://pkg.go.dev/encoding/json#Decoder.Token、https://pkg.go.dev/encoding/json#Decoder.More、https://pkg.go.dev/encoding/json#Decoder.Decode。
Go JSON 类型错误怎样定位字段和输入偏移
- 上一篇
- Go JSON 类型错误怎样定位字段和输入偏移
- 下一篇
- Go 十六进制字符串包含空格时怎样处理
-
- Golang · Go问答 | 59分钟前 | CGO · 内存管理 · Go问答 · 排错 · Go Go指针 CGO runtime/cgo.Handle C指针 runtime.Pinner cgocheck
- Go cgo 传递指针触发检查怎么办
- 392浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go http.Transport 怎样给 CONNECT 代理请求添加请求头
- 175浏览 收藏
-
- Golang · Go问答 | 1小时前 | 单元测试 · testing · Go问答 · Go Go测试 testing.Short go test -short 慢测试
- Go testing.Short 怎样让慢测试只在完整模式运行
- 161浏览 收藏
-
- Golang · Go问答 | 1小时前 | 标准库 · 错误处理 · Go问答 · Go 字符串处理 十六进制 encoding/hex hex.DecodeString
- Go 十六进制字符串包含空格时怎样处理
- 473浏览 收藏
-
- Golang · Go问答 | 1小时前 | 标准库 · JSON · Go问答 · Go encoding/json offset json.Unmarshal Field JSON排错 UnmarshalTypeError
- Go JSON 类型错误怎样定位字段和输入偏移
- 414浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 解析相对请求目标失败怎么办
- 204浏览 收藏
-
- Golang · Go问答 | 7小时前 |
- Go 子进程继承文件描述符数量不对怎么办
- 356浏览 收藏
-
- Golang · Go问答 | 7小时前 |
- Go 信号处理退出偶发丢失怎么办
- 139浏览 收藏
-
- Golang · Go问答 | 7小时前 |
- Go plugin 加载符号找不到怎么办
- 470浏览 收藏
-
- Golang · Go问答 | 7小时前 |
- Go 模块路径显示不一致时怎样定位 replace
- 215浏览 收藏
-
- Golang · Go问答 | 7小时前 |
- Go 依赖下载被代理或校验服务阻断怎么办
- 228浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 512次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 572次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 597次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 549次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 366次使用
-
- golang中gin框架接入jwt使用token验证身份
- 2023-01-17 155浏览
-
- gogin+token(JWT)验证实现登陆验证
- 2023-01-01 296浏览
-
- golang jwt+token验证的实现
- 2023-02-21 188浏览
-
- Go SSE 写出后浏览器为什么还收不到:ResponseController.Flush、代理缓冲与写超时
- 2026-08-22 430浏览
-
- Go 1.27 go test -json OutputType 怎么解析:区分错误、续行与帧
- 2026-08-31 266浏览
