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

Go JSON Decoder 处理连续对象流的边界设计

来源:17golang原创

时间:2026-10-15 07:51:44 134浏览 收藏

处理 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。

Go JSON Decoder 从输入流到 Message 对象并以 io.EOF 表示结束的静态结构说明图
图1:输入流、Decoder、Message 与 io.EOF 的边界关系说明图,不是运行截图。

用 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
}
单对象、连续顶层对象流和 JSON 数组选择 Decode、More、Token 的静态关系说明图
图2:单对象、连续对象流和数组容器对应的 Decoder 接口关系说明图,不是运行截图。

把读取策略封装成稳定边界

在 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 的组合。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>