当前位置:首页 > 文章列表 > Golang > Go问答 > Go Decoder 连续读取遇到 EOF 的边界判断

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

来源:17golang原创 2026-10-28 15:13:39 0浏览 收藏

连续处理 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删除
Go net/http/pprof 怎样给诊断入口加访问控制Go net/http/pprof 怎样给诊断入口加访问控制
上一篇
Go net/http/pprof 怎样给诊断入口加访问控制
Go strconv.IsPrint 判断字符是否适合可打印日志的方案
下一篇
Go strconv.IsPrint 判断字符是否适合可打印日志的方案
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    758次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    755次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    837次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    803次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    635次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码