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

Token 返回的内容主要有两类:json.Delim 表示 { } [ ],其他 token 表示字符串、数字、布尔值或 null。字段名本身也是字符串 token,所以不能用“读到字符串后下一个 token 就是字段值”来判断对象是否结束,真正可靠的信号是分隔符的嵌套层级。
| token | 结构含义 | 边界动作 |
|---|---|---|
{ 或 [ | 进入对象或数组 | 深度加一 |
} 或 ] | 离开当前对象或数组 | 深度减一 |
| 字符串、数字、布尔值、null | 普通 JSON 值 | 不改变深度 |
例如 {"meta":{"tags":["go"]},"id":7} 中,tags 数组结束只让深度回到对象内部;最后一个 } 才把对象深度降到 0。标准库同时保证返回的分隔符正确嵌套,遇到异常分隔符会返回错误,因此应用层主要负责判断根值类型和记录错误位置。
用 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 只表示当前对象或数组里是否还有元素,不能拿它判断多个顶层对象是否还有下一个;顶层循环应以 Decode 的 io.EOF 为结束条件。
边界、错误和缓冲区怎么复查
生产代码中,先决定你要的是“结构位置”还是“业务对象”。前者可以使用 Token 和偏移;后者直接 Decode,避免自己维护一套 JSON 重建器。两者都要对错误做分层记录:语法错误说明输入结构坏了,字段反序列化错误说明结构合法但不符合业务类型。
- 不要把
InputOffset当字符下标。 它是输入流字节偏移,中文和 ASCII 的字节长度不同。 - 不要用
More扫描顶层流。 它只对当前对象或数组有效,连续顶层值应看Decode和io.EOF。 - 不要假设 Decoder 没有预读。 标准库允许它从底层 Reader 多读一些数据;若要接管剩余缓冲,需理解
Buffered()的生命周期。 - 面对超大对象要设置资源边界。 Token 也会持续读取输入,业务层应限制单条值大小、记录异常偏移,并决定是否丢弃连接。
常见问题
Token 能直接返回对象的原始 JSON 字节吗?
不能。它返回 token,不提供当前对象的原始切片。要保留完整值,使用 Decode(&raw);要做字节定位,可结合 InputOffset 和你自己的受控缓冲区。
嵌套数组的 ] 到来时为什么不能算对象结束?
因为它只关闭数组这一层。只有根对象的 } 让深度从 1 降到 0,才说明对象整体结束。
连续 JSON 对象之间必须有换行吗?
不必须。Decoder 读取的是连续顶层 JSON 值,空白包括换行只是可选分隔;没有空白的 {"id":1}{"id":2} 也可以逐个 Decode。
怎样判断扫描出来的区间是否可靠?
让扫描器遇到 EOF、非法分隔符和未闭合结构时都返回错误,并把 InputOffset 写入日志。不要只根据计数器成功归零就忽略 token 错误。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
334 收藏
-
Golang · Go问答 | 48分钟前 | 后端开发 · Go问答 · 接口兼容 · JSON处理 · 请求校验 · Go DisallowUnknownFields json.Decoder JSON兼容 旧客户端228 收藏
-
283 收藏
-
236 收藏
-
161 收藏
-
124 收藏
-
168 收藏
-
273 收藏
-
Golang · Go问答 | 5小时前 | Go问答 · Go构建 · build constraints · 跨平台编译 · GOARCH GOOS go list Go build tag Go文件名后缀461 收藏
-
254 收藏
-
161 收藏
-
411 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习