Go JSON Decoder 处理连续对象流的边界设计
处理 JSON 文件或 HTTP 响应体时,最容易混淆的是“一个 JSON 值”和“连续出现的多个 JSON 值”。Go 的 json.Decoder 面向的是输入流:每次调用 Decode 读取下一个完整值,读到输入末尾时返回 io.EOF。因此,连续对象流不应该被强行拼成一个大结构体,而应把“下一个对象是否存在”交给解码循环判断。
这篇只讨论三种边界:单个对象、空白分隔的多个顶层对象,以及数组容器中的多个对象。核心做法是先选择与输入形态匹配的读取模型,再把 io.EOF 和真正的语法或类型错误分开处理。
先看清输入到底是哪一种
下面三种文本看起来都像“很多对象”,但边界语义不同:
- 单对象:
{"id":1},适合一次Decode。 - 连续对象流:
{"id":1} {"id":2},两个顶层 JSON 值由空白分隔,适合循环调用Decode。 - 数组:
[{"id":1},{"id":2}],对象属于同一个数组容器,可用Token和More感知容器边界。
json.NewDecoder 自己管理缓冲,而且可能从底层 io.Reader 预读超出当前 JSON 值的部分。调用方应复用同一个 Decoder,不要为了“读下一个对象”重新创建 Reader 或 Decoder。

用 Decode 循环读取连续对象
空白分隔的顶层对象流没有数组的开闭括号,最小可靠模式就是“先 Decode,再判断错误”。只有 io.EOF 表示正常读完;其它错误都应该保留上下文并返回。
package main
import (
"errors"
"fmt"
"io"
"strings"
"encoding/json"
)
type Message struct {
ID int `json:"id"`
Text string `json:"text"`
}
func readMessageStream(input io.Reader) ([]Message, error) {
dec := json.NewDecoder(input)
var messages []Message
for {
var msg Message
// Decode 每次只消费一个顶层 JSON 值,Decoder 在循环中保持不变。
err := dec.Decode(&msg)
if errors.Is(err, io.EOF) {
// EOF 代表没有下一个对象,是流式读取的正常结束。
return messages, nil
}
if err != nil {
// 语法错误或类型错误不能当作自然结束返回。
return nil, fmt.Errorf("decode message: %w", err)
}
messages = append(messages, msg)
}
}
func main() {
input := strings.NewReader(`{"id":1,"text":"first"}
{"id":2,"text":"second"}`)
messages, err := readMessageStream(input)
if err != nil {
panic(err)
}
fmt.Println(messages)
}
这个循环有两个重要决定。第一,空白、换行和多个对象之间的间隔由 Decoder 处理,业务代码不需要自己切字符串。第二,io.EOF 只在“已经读完最后一个完整对象,且没有下一个值”时结束;如果最后一个对象缺少右括号,返回的会是语法错误,而不是 EOF。
把 EOF、空输入和坏数据分开
边界处理最好在读取函数内部完成,让调用方只看到“对象列表或错误”。下面这个变化表可以避免把异常吞掉:
| 输入状态 | Decode 返回 | 函数行为 |
|---|---|---|
| 一个或多个完整对象,随后结束 | 最后一次为 io.EOF | 返回已读取对象和 nil |
| 只有空白 | 第一次即为 io.EOF | 返回空切片和 nil,由业务决定是否允许 |
| 对象缺字段但 JSON 语法完整 | 通常为 nil,未提供字段保留零值 | 需要额外业务校验必填字段 |
| 逗号、引号或括号损坏 | 非 EOF 错误 | 包装错误并终止读取 |
| 对象类型与字段类型不匹配 | 非 EOF 错误 | 按输入错误返回,不继续消费后续内容 |
“缺字段”和“解码失败”不是一回事。标准库会把缺少的结构体字段保留为零值;如果接口要求 id 必须大于零,应在 Decode 成功后单独校验,而不要把 EOF 当作业务判断。
func readOne(input io.Reader) (Message, error) {
var msg Message
dec := json.NewDecoder(input)
// 单对象接口只关心第一个值,Decode 成功后再做业务字段检查。
if err := dec.Decode(&msg); err != nil {
return Message{}, fmt.Errorf("decode one message: %w", err)
}
if msg.ID
数组输入要用容器边界
如果上游明确发送的是 JSON 数组,就不要把数组误判为空白分隔的对象流。数组有开括号和闭括号,可以先读取 [,再用 More 判断是否还有元素,最后消费 ]。
func readMessageArray(input io.Reader) ([]Message, error) {
dec := json.NewDecoder(input)
var messages []Message
// Token 先确认顶层确实是数组,而不是一个普通对象。
token, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read array start: %w", err)
}
if delim, ok := token.(json.Delim); !ok || delim != '[' {
return nil, fmt.Errorf("expected JSON array, got %v", token)
}
for dec.More() {
var msg Message
// More 只表示数组中仍有元素,具体对象仍由 Decode 解析。
if err := dec.Decode(&msg); err != nil {
return nil, fmt.Errorf("decode array item: %w", err)
}
messages = append(messages, msg)
}
// 读取闭括号,把数组容器边界消费完整。
if token, err = dec.Token(); err != nil {
return nil, fmt.Errorf("read array end: %w", err)
}
if delim, ok := token.(json.Delim); !ok || delim != ']' {
return nil, fmt.Errorf("expected JSON array end, got %v", token)
}
return messages, nil
}

把读取策略封装成稳定边界
在 HTTP body、文件或消息消费者里,读取函数通常还需要限制单次输入的来源和业务上限。可以把 Reader 交给函数,把“解码策略”固定下来;如果需要限制字节数,应在更外层使用带上限的 Reader,并让错误继续向上传播。
func consumeStream(input io.Reader, maxItems int, handle func(Message) error) error {
dec := json.NewDecoder(input)
for count := 0; count
这个封装把三种结果分开:没有下一个值是正常结束,格式或类型错误是输入失败,超过业务上限是策略失败。对于长连接或持续生产的输入,函数也可以把 maxItems 换成上下文取消和单条处理回调,但不要改变 EOF 的含义。
选择方式时记住这张清单
- 输入是多个没有数组括号的顶层对象:复用一个
Decoder,循环Decode。 - 输入是一个数组:先用
Token确认[,用More读取元素,最后消费]。 io.EOF只表示没有下一个完整值,不能吞掉其它错误。- 结构体缺字段属于业务校验问题,字段类型错误属于解码错误,需分别处理。
- 连续处理时优先逐条消费,是否收集到切片取决于内存预算和业务需要。
Go 官方 encoding/json 文档将 Decoder 定义为从输入流读取 JSON 值;本文的边界设计也建立在这个契约上。需要进一步确认方法语义时,可查看 https://pkg.go.dev/encoding/json 和 https://go.dev/src/encoding/json/stream.go。
相关问题
空输入应该报错吗? 对连续对象流来说,第一次 Decode 返回 EOF 通常表示空流;是否允许空流是业务规则,不是 JSON 解码器的错误。
为什么不把多个对象先读成 []byte 再 Unmarshal? 那样需要先确定完整边界并占用额外内存;输入本身是流时,Decoder 更适合逐个读取。
数组能不能直接用 Decode 读成切片? 可以,在数组规模可控时一次 Decode 到切片更简单;需要逐条消费或控制内存时,再使用 Token、More 和 Decode 的组合。
Go encoding/csv 流式读取大文件的内存控制方案
- 上一篇
- Go encoding/csv 流式读取大文件的内存控制方案
- 下一篇
- Go JSON 请求兼容未知字段的解码策略
-
- Golang · Go教程 | 13分钟前 |
- Go Benchmark 测量分配次数的基准设计
- 306浏览 收藏
-
- Golang · Go教程 | 14分钟前 |
- Go fuzz 测试发现解析器边界输入的写法
- 196浏览 收藏
-
- Golang · Go教程 | 17分钟前 |
- Go httptest.Server 模拟外部 API 的测试边界
- 490浏览 收藏
-
- Golang · Go教程 | 21分钟前 |
- Go 表驱动测试组织多组输入输出
- 362浏览 收藏
-
- Golang · Go教程 | 21分钟前 |
- Go atomic.Int64 统计并发指标的读取一致性
- 267浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Go channel 信号量限制并发任务数的实现
- 111浏览 收藏
-
- Golang · Go教程 | 28分钟前 |
- Go worker pool 的优雅停止与任务回收方案
- 130浏览 收藏
-
- Golang · Go教程 | 29分钟前 |
- Go SQL 事务中间错误的回滚结构
- 401浏览 收藏
-
- Golang · Go教程 | 31分钟前 |
- Go database/sql 查询超时的 context 传递方式
- 344浏览 收藏
-
- Golang · Go教程 | 33分钟前 |
- Go database/sql DB.Conn 怎样为一次操作绑定独立连接
- 209浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 478次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 535次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 555次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 506次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 328次使用
-
- 接口返回 200 但前端仍报错怎么办:从响应格式到跨域一步步排查
- 2026-06-14 332浏览
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- golang生成JSON以及解析JSON
- 2023-01-17 329浏览
-
- Go如何实现json字符串与各类struct相互转换
- 2023-01-07 377浏览
-
- Go中使用gjson来操作JSON数据的实现
- 2023-01-07 141浏览
