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

Go Decoder 连续读取遇到 EOF 的边界判断

来源:17golang原创

时间:2026-10-28 15:13:39 269浏览 收藏

连续处理 JSON 消息时,json.Decoder 返回 io.EOF 不代表上一条 JSON 有问题,而是当前输入里已经没有下一个完整的顶层值。正确做法是把它当作“正常结束”分支;只要返回的是语法错误、类型错误或底层读取错误,就应当保留并上报,不能用一个宽泛的 EOF 判断把故障吞掉。

官方地址:https://pkg.go.dev/encoding/json#Decoder

在空白分隔的 JSON 流中,循环调用 Decode;err == nil 表示成功得到一个值,err == io.EOF 表示流结束,其它错误都不是“读完了”。

Decode 返回 EOF 到底说明了什么

Decoder 面向的是输入流,而不是只接收一个 JSON 文档。每次 Decode 读取下一个 JSON 值,因此下面这样的输入可以被连续读取:

{"id":1}
{"id":2}
{"id":3}

前三次调用分别得到三个对象,第四次调用才会在已经跳过尾部空白后得到 io.EOF。空输入或只包含空白的输入也没有可交付的值,第一次调用就可能返回 EOF;这与“收到一个空对象”不同,空对象应写成 {} 并成功解码。

这里的关键是“没有下一个值”。它不是“底层 Reader 恰好被调用了一次并返回 EOF”,因为官方文档明确说明 Decoder 有自己的缓冲,可能会从 Reader 多读取一些数据。业务判断应围绕 Decode 的结果,而不要根据 Reader 的调用次数猜测消息是否结束。

Go Decoder 将成功、EOF 和其他错误分开的结果关系说明图
图1:结果关系说明图,展示连续 Decode 中成功、EOF 与其它错误的边界。

连续 Decode 应该怎样写分支

最小可靠循环可以把三种结果分开:成功继续处理,EOF 正常收尾,其它错误立即返回。示例中的结构体只是演示目标类型,实际项目可换成事件或请求对象。

package main

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

type Event struct {
	ID   int    `json:"id"`
	Kind string `json:"kind"`
}

func readEvents(input io.Reader) ([]Event, error) {
	dec := json.NewDecoder(input)
	var events []Event

	for {
		var event Event
		err := dec.Decode(&event)
		switch {
		case err == nil:
			// 成功拿到一个完整值,只有此时才把目标对象加入结果。
			events = append(events, event)
		case err == io.EOF:
			// 没有下一个顶层值是正常收尾,不要把它当成失败。
			return events, nil
		default:
			// 语法、类型或 Reader 错误都要保留,避免静默丢消息。
			return nil, fmt.Errorf("decode event: %w", err)
		}
	}
}

func main() {
	input := strings.NewReader(`{"id":1,"kind":"created"}
{"id":2,"kind":"updated"}`)
	events, err := readEvents(input)
	if err != nil {
		panic(err)
	}
	fmt.Println(len(events)) // 这里会得到 2,EOF 只负责结束循环。
}

不要只写 if err != nil { break }:这样会把半截 JSON、字段类型不匹配,甚至 Reader 的真实故障都伪装成“读完了”。同样,也不要在每次循环前复用上一次已经填充的对象而不清理状态;把新变量放在循环体内,可以避免失败时误用旧值。

空白、截断和类型错误要分开

判断 EOF 时最容易混淆的是“输入没有内容”和“输入内容不完整”。尾部换行、空格属于合法的结束留白,Decode 在跳过它们后返回 EOF。相反,输入如果在 JSON 值中间结束,例如 {"id":,解码器会报告不完整输入相关的错误,而不是把它当作一个正常的流结束。

还有三类错误需要继续向上返回:

  • JSON 语法错误:引号、逗号、括号等结构损坏,说明数据本身不可解析。
  • 目标类型错误:例如把字符串放入 int 字段,值可能已部分写入目标对象,但本次消息不能按成功处理。
  • 底层读取错误:网络连接或文件读取失败,不等于业务上没有下一条消息。

如果消息协议要求“至少收到一条记录”,可以在循环结束后检查 len(events);如果空流合法,就让空切片和 nil 错误表示正常结果。这个约束属于业务层,不应通过修改 EOF 判断来表达。

数组内部和 token 级读取的边界

当输入是一个 JSON 数组时,通常应先读取开括号,再用 More 判断数组中是否还有元素,最后读取闭括号。此时数组内部的“没有更多元素”不是靠把 EOF 当作数组结束,而是由 JSON 的结构分隔符表达。

func readArray(input io.Reader) ([]Event, error) {
	dec := json.NewDecoder(input)
	var events []Event

	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 array start, got %v", token)
	}

	for dec.More() {
		var event Event
		if err := dec.Decode(&event); err != nil {
			// 数组未闭合或元素损坏属于输入错误,不当作正常结束。
			return nil, fmt.Errorf("decode array item: %w", err)
		}
		events = append(events, event)
	}

	if _, err := dec.Token(); err != nil {
		// 读取闭括号,确保数组结构确实结束。
		return nil, fmt.Errorf("read array end: %w", err)
	}
	return events, nil
}

需要逐个查看字段或分隔符时再使用 Token;它在输入流末尾明确返回 nil, io.EOF。如果一个 Decoder 后面还要接着读取非 JSON 数据,要注意它可能已经预读,官方提供的 Buffered 只能在下一次 Decode 或 Token 前使用,不能把底层 Reader 的当前位置当作精确边界。

Go Decoder 数组 More 与 Token 结构边界说明图
图2:结构说明图,展示数组边界由 More、Token 和元素 Decode 共同表达。

最后检查这三个结果

连续读取的判断可以压缩成三句话:nil 是“本条值已成功交付”,io.EOF 是“没有下一条值且允许收尾”,其它错误是“输入或目标处理失败”。只要把这三个结果分开,空输入、尾部空白、截断 JSON 和字段类型错误就不会互相伪装。

  • 多条顶层 JSON:循环 Decode,EOF 收尾。
  • 单个数组:用 More 表达数组元素边界,用 Token 读取括号。
  • 流式协议:把 EOF 是否允许作为业务约束放在循环外,不能吞掉真实错误。

常见问题

第一次 Decode 就返回 EOF,是不是 Decoder 配置错了?

不一定。输入为空或只有空白时,这是正常结果;如果业务要求必须有一条消息,再由调用方把“零条记录”判为业务错误。

可以用所有非 nil 错误来结束循环吗?

不可以。只有明确识别为 EOF 才能正常结束;语法错误、类型错误和底层错误都应记录或返回。

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