Go JSON 流式解码为什么卡在输入末尾
Go JSON 流式解码卡在输入末尾,通常是程序还在等底层 Reader 给出更多字节或 EOF,而不是解析器没有认出已经读到的内容。数组的右方括号、一个对象的右花括号、发送端暂时没有新数据,都不等于整条流结束;再次调用 Decode、Token,甚至不合场景的 More,都可能继续等待。
官方地址:https://pkg.go.dev/encoding/json
先决定协议以什么作为结束条件:一个数组结束就停止、一个消息完成就返回,还是连续读到写端关闭。只有有限的值流适合以 io.EOF 作为正常结束标志;开放连接必须另有消息边界和等待期限。
明明最后一个对象已经收到,为什么还在等?
假设数据生产者写出一个完整对象后,仍保留管道写端或网络连接。当前对象可能已经被 Decode 成功解析,但循环又调用了一次 Decode,希望确认是否还有下一条。读端无法知道对方是“以后不再发送”还是“稍后还要发送”,所以等待并不奇怪。
另有一种情况发生在顶层数字上:只发送数字字符、没有分隔字符或 EOF 时,解析器可能需要继续读取,才能确定数字是否还会延长。不要把“当前界面已经显示全部字符”当成 Reader 已经结束。对象和数组有结构结束符,顶层数字的边界则更容易依赖后续输入。

More 能代替顶层 EOF 判断吗?
不能。More 的职责是在当前数组或对象内判断还有没有元素,不是判断整个任意 JSON 流是否结束。Token 则用于消费结构标记等 token。遍历数组时,先用 Token 读左方括号,再在 More 循环中解码元素,循环后还要用 Token 消费右方括号。
尤其不要在已经读完数组结束符之后,再写一个“为了保险”的 More 或 Token 来探测整条开放连接。它可能正是末尾等待的来源。More 只有布尔结果,也不能独自证明输入没有截断;数组关闭标记对应的读取错误必须保留。
数组消息和连续值流应该怎样写?
下面的原创程序提供两个明确模式:array 消费一个数组后返回;sequence 消费顶层对象序列,直到真正 EOF。两种模式不能凭“哪个不会卡住”随意互换,而要与发送方协议一致。代码和命令均未在本文制作过程中运行,后面的行为说明是供读者复现时对照的预期。
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
)
type Record struct {
ID int `json:"id"`
}
func emit(out io.Writer, v Record) error {
// 示例逐条输出,不把整个数组累积到内存。
_, err := fmt.Fprintf(out, "id=%d\n", v.ID)
return err
}
func readArray(dec *json.Decoder, out io.Writer) error {
token, err := dec.Token()
if err != nil { return fmt.Errorf("read opening: %w", err) }
if token != json.Delim('[') { return errors.New("expected array") }
for dec.More() {
var v Record
if err := dec.Decode(&v); err != nil {
return fmt.Errorf("read array item: %w", err)
}
if err := emit(out, v); err != nil { return err }
}
// More 为 false 不代表数组完整:必须消费并检查结束符。
token, err = dec.Token()
if err != nil { return fmt.Errorf("read closing: %w", err) }
if token != json.Delim(']') { return errors.New("expected closing bracket") }
// 此模式以右方括号为消息边界,不额外等待底层 EOF。
return nil
}
func readSequence(dec *json.Decoder, out io.Writer) error {
for {
var v Record
err := dec.Decode(&v)
if errors.Is(err, io.EOF) { return nil }
if err != nil {
// 截断、类型错误和读错误不能当作正常完成。
return fmt.Errorf("read stream item: %w", err)
}
if err := emit(out, v); err != nil { return err }
}
}
func run(args []string) error {
if len(args) != 1 { return errors.New("usage: array | sequence") }
// 全程保留同一个 Decoder,避免丢失其预读缓冲。
dec := json.NewDecoder(os.Stdin)
switch args[0] {
case "array":
return readArray(dec, os.Stdout)
case "sequence":
return readSequence(dec, os.Stdout)
default:
return errors.New("unknown mode")
}
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
array 模式并不保证输入只有这个数组:它故意在消费右方括号后停止,不额外确认后面没有第二个值。它适用于协议把数组当成一条消息的场景。如果要求有限文档只能包含一个数组,应在已确定有限的输入边界内再尝试解码第二个值,只有 EOF 才说明没有尾随值;把这一步直接放在没有结束约定的长连接上,仍然可能等待。
sequence 模式只把直接的流结束当作正常完成。半个对象导致的 unexpected EOF、字段类型不符或其他读取错误,都应作为失败返回。这个示例采用逐条输出,因此中途错误可能已经输出前面的记录;业务若要求整批原子提交,应单独设计事务或暂存,而不是误以为流式解码天然保证回滚。

怎样分辨是在等 EOF,还是输入真的坏了?
保存代码为 main.go 后,可用有限管道输入进行对照。命令中的生产者写完会退出,管道因此有明确结束,不需要人工输入 EOF。
# 单条数组消息:预期输出两条记录并在数组结束后返回
printf '%s' '[{"id":21},{"id":34}]' | go run main.go array
# 顶层值序列:换行分隔便于阅读,写端退出后得到 EOF
printf '%s\n' '{"id":21}' '{"id":34}' | go run main.go sequence
# 截断输入:预期报错并以非零状态退出,而不是当作正常完成
printf '%s' '[{"id":21}' | go run main.go array
前两条预期按顺序输出 id=21、id=34;第三条即使已经输出 id=21,也仍应在缺少右方括号时报告错误。若换成终端手动输入,回车只是写入换行,并不关闭标准输入;sequence 模式继续等待下一条是符合约定的。
| 输入场景 | 应采用的结束条件 | 常见误判 |
|---|---|---|
| 有限对象序列 | Decode 返回 io.EOF | 把任何错误都当成结束 |
| 一个数组作为消息 | 正确消费右方括号 | 循环后继续等待连接关闭 |
| 持久连接上的多条消息 | 明确帧边界或消息协议 | 每条消息都等整条连接 EOF |
| 输入截断 | 报告结构不完整 | More 为 false 就认定成功 |
写端不关闭,超时和 Context 能自动解决吗?
使用 io.Pipe 传递有限数据时,生产者完成后应关闭 PipeWriter;失败应通过 CloseWithError 传递。读端结束或不再消费时也要有对应清理,避免生产者还卡在写入。增加 bufio.Reader 不会替发送方制造 EOF。
Decoder 没有独立的 Context 参数。检查 ctx.Err 或把 Decode 放到 goroutine 并不能自动中断底层阻塞读取,外层超时返回后还可能遗留 goroutine。对自己拥有的 net.Conn,可设置读取截止时间,或按连接生命周期关闭连接来解除等待;截止时间影响正在阻塞的读取,超时要作为读取失败处理,而不是完整 JSON 的证明。HTTP 场景则应配置请求与 Body 的生命周期,不要把 HTTP 长连接是否保持和当前响应体是否结束混为一谈。
每次读一条都重新创建 Decoder 可以吗?
不宜在同一底层流上这样做。Decoder 可以预读后续内容,丢掉旧 Decoder 再直接读原 Reader,可能丢失它已经缓存的字节。连续消息优先复用同一 Decoder。
加一个换行就一定不再等待吗?
不一定。换行能为数字等输入提供分隔,但不会关闭流;读到下一条的循环仍会等。若协议是严格的一行一条,应按行取得有长度限制的完整帧,再解码这一帧,并明确空行、超长行及 EOF 的处理。
限制最大字节数能当作消息边界吗?
不能随意这样做。上限用于限制资源消耗,不等于对方真的结束;达到限制产生的截断不能当成正常消息完成。边界、大小上限和等待期限需要分别定义。
参考:json.Decoder、More 与 Token;PipeReader 的阻塞与 EOF;网络连接读取截止时间。
Go multipart 文件名为什么不能直接信任
- 上一篇
- Go multipart 文件名为什么不能直接信任
- 下一篇
- Go bytes.Equal 和字符串比较的边界是什么
-
- Golang · Go问答 | 39分钟前 | 垃圾回收 · Go问答 · Go 延迟 GC GOGC GOMEMLIMIT
- Go GC 控制参数改了为什么延迟反而升高
- 333浏览 收藏
-
- Golang · Go问答 | 40分钟前 | go ·
- Go runtime.ReadMemStats 为什么不能直接代表 RSS
- 423浏览 收藏
-
- Golang · Go问答 | 40分钟前 |
- Go trace 和 pprof 应该先看哪一个
- 179浏览 收藏
-
- Golang · Go问答 | 43分钟前 |
- Go slog JSON 日志为什么混入不可序列化字段
- 470浏览 收藏
-
- Golang · Go问答 | 44分钟前 |
- Go time.Timer.Stop 返回 false 后怎样避免重复处理到期事件
- 246浏览 收藏
-
- Golang · Go问答 | 49分钟前 |
- Go 服务器优雅退出为什么仍有请求被截断
- 451浏览 收藏
-
- Golang · Go问答 | 56分钟前 |
- Go http.Request FormValue 为什么会吞掉解析错误
- 129浏览 收藏
-
- Golang · Go问答 | 56分钟前 | 性能排查 · Go问答 · Go io.Writer 短写 WriteTo net.Buffers
- Go net.Buffers WriteTo 为什么可能只写出部分数据
- 196浏览 收藏
-
- Golang · Go问答 | 58分钟前 |
- Go WaitGroup Add 放错位置为什么会 panic
- 325浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 599次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 640次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 672次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 638次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 458次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go JSONJSON null 解码到指针后状态不符合预期怎么办
- 2026-10-18 501浏览
-
- Go crypto/rand.Text 的长度为什么不是固定字符数
- 2026-10-04 501浏览
-
- Go strings.ToValidUTF8 清洗日志内容的边界
- 2026-10-03 501浏览

