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

Go encoding/json 怎样用 Decoder 流式读取多个对象完成一个可复用的最小示例

来源:17golang原创

时间:2026-11-07 03:36:19 318浏览 收藏

当输入不是一个很大的 JSON 数组,而是连续写入的多个对象时,最小而稳妥的做法是为同一个 io.Reader 创建一个 json.Decoder,然后反复调用 Decode。每次调用消费一个对象;读到 io.EOF 表示对象流正常结束,中途出现的语法错误则应该原样返回。

官方文档:https://pkg.go.dev/encoding/json

把“读完了”和“读坏了”分开处理:只有 io.EOF 是成功结束,其他错误都要保留给调用方。下面的函数还会把空输入当作空集合,适合复用在文件、HTTP 响应体和管道输入上。

先分清连续对象和 JSON 数组

下面两种输入看起来都包含多个对象,但读取方式不同。数组是一个完整 JSON 值,通常可以一次解码到切片;连续对象则是多个相邻的 JSON 值,中间允许有空白字符,应该让同一个 Decoder 逐个消费。

[{"id":1,"name":"北京"},{"id":2,"name":"杭州"}]
{"id":1,"name":"北京"}
{"id":2,"name":"杭州"}

如果把第二种输入交给 json.Unmarshal,它会把整段文本当成一个 JSON 文档,遇到后面的对象就无法继续。Decoder 的价值在于它维护输入流状态,让下一次 Decode 从上一次结束的位置继续读取。

连续 JSON 对象通过 io.Reader 进入 json.Decoder 并分别解码为 Go 结构体集合的静态结构说明图
图1:结构说明图,展示 io.Reader、json.Decoder、Decode 和目标结构体之间的静态关系;不是运行截图。

把 Decode 循环封装成可复用函数

为了让调用方不必重复写 EOF 判断,可以把读取逻辑封装成泛型函数。函数只负责“从流中读对象”,不绑定文件或网络来源;调用方传入任何实现了 io.Reader 的对象即可。

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
)

type Record struct {
	// 字段名与 JSON 键对应,Decode 会把每个对象填入一个新值。
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// DecodeStream 逐个读取 JSON 对象,适用于连续 JSON 值而不是单个数组。
func DecodeStream[T any](r io.Reader) ([]T, error) {
	dec := json.NewDecoder(r) // 一个 Reader 只配一个 Decoder,保持流位置连续。
	items := make([]T, 0)

	for index := 0; ; index++ {
		var item T
		err := dec.Decode(&item) // 每次只消费一个 JSON 值。
		if errors.Is(err, io.EOF) {
			// EOF 表示正常读完;空输入也自然得到空切片。
			return items, nil
		}
		if err != nil {
			// 保留对象序号和底层错误,方便定位坏数据所在位置。
			return nil, fmt.Errorf("decode object %d: %w", index, err)
		}
		items = append(items, item)
	}
}

这里没有把 io.EOF 当成普通失败:它是循环的结束信号。相反,第二个对象缺少右括号、字段类型不匹配等错误都会带着对象序号返回,调用方可以决定重试、丢弃整批还是记录坏输入。

用小输入确认空白、EOF 和错误边界

最小调用方可以使用 strings.NewReader 构造连续对象。真实项目中把它替换为打开的文件、HTTP 响应体或消息管道即可,读取函数本身不需要改变。

package main

import (
	"fmt"
	"strings"
)

func main() {
	input := strings.NewReader(`
{"id":1,"name":"北京"}

{"id":2,"name":"杭州"}
`)

	items, err := DecodeStream[Record](input)
	if err != nil {
		// 这里把中途的语法错误交给上层,而不是把部分结果当成完整批次。
		fmt.Println("read failed:", err)
		return
	}

	// 成功时 items 只包含已经完整解码的对象。
	fmt.Printf("read %d records: %+v\n", len(items), items)
}
输入情况函数结果处理建议
空输入或末尾空白空切片或已收集对象,error 为 nil视为正常结束
连续对象全部合法按输入顺序返回对象集合交给业务处理
中途 JSON 不完整error 包含对象序号记录原始错误,不伪装成 EOF
顶层是一个数组也能 Decode 一次按数组协议另写切片解码路径
json.Decoder 将正常 EOF、合法对象和中途语法错误分成不同输入边界的静态关系说明图
图2:边界说明图,区分合法对象、末尾 EOF 与中途语法错误三种结果;不是运行结果截图。

接入文件和 HTTP 响应时注意什么

Decoder 读取的是流,不会替调用方关闭文件或响应体。文件和响应体仍要由创建它们的代码负责 Close。另外,官方文档说明 Decoder 可能从底层 Reader 预读超过当前 JSON 值;如果同一个 Reader 后面还要交给别的解析器,不要在 Decoder 和另一个读取器之间随意切换,必要时统一由 Decoder 管理后续内容。

如果数据规模很大,返回 []T 仍然会把所有对象留在内存中。此时可以把函数改成“每次 Decode 后调用一个处理函数”,让业务消费一个对象就释放本轮引用;但 EOF 判断、错误包装和单 Decoder 原则保持不变。

相关问题

连续 JSON 对象必须用换行分隔吗?

不必须,JSON 空白可以是换行、空格或制表符;换行只是日志和批处理文件更容易阅读。对象之间不能直接拼成一个无效的对象文本。

为什么不能用 Decoder.More 判断顶层对象流?

More 主要用于数组或对象内部的下一个元素判断。顶层连续对象应直接循环调用 Decode,以 io.EOF 结束。

读到一半报错时要不要返回已读对象?

示例选择返回 nil 和错误,避免调用方误把部分批次当成完整数据。如果业务允许部分成功,可以改为同时返回 items,但必须在接口说明中明确这一语义。

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