当前位置:首页 > 文章列表 > Golang > Go问答 > Go JSON 流式解码为什么卡在输入末尾

Go JSON 流式解码为什么卡在输入末尾

来源:17golang原创 2026-10-23 03:05:18 0浏览 收藏

Go JSON 流式解码卡在输入末尾,通常是程序还在等底层 Reader 给出更多字节或 EOF,而不是解析器没有认出已经读到的内容。数组的右方括号、一个对象的右花括号、发送端暂时没有新数据,都不等于整条流结束;再次调用 Decode、Token,甚至不合场景的 More,都可能继续等待。

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

先决定协议以什么作为结束条件:一个数组结束就停止、一个消息完成就返回,还是连续读到写端关闭。只有有限的值流适合以 io.EOF 作为正常结束标志;开放连接必须另有消息边界和等待期限。

明明最后一个对象已经收到,为什么还在等?

假设数据生产者写出一个完整对象后,仍保留管道写端或网络连接。当前对象可能已经被 Decode 成功解析,但循环又调用了一次 Decode,希望确认是否还有下一条。读端无法知道对方是“以后不再发送”还是“稍后还要发送”,所以等待并不奇怪。

另有一种情况发生在顶层数字上:只发送数字字符、没有分隔字符或 EOF 时,解析器可能需要继续读取,才能确定数字是否还会延长。不要把“当前界面已经显示全部字符”当成 Reader 已经结束。对象和数组有结构结束符,顶层数字的边界则更容易依赖后续输入。

JSON 值边界、容器结束符和流结束条件的关系
图1:输入边界静态结构图。容器完整与写端关闭是两层不同条件,不是实测时序图。

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、字段类型不符或其他读取错误,都应作为失败返回。这个示例采用逐条输出,因此中途错误可能已经输出前面的记录;业务若要求整批原子提交,应单独设计事务或暂存,而不是误以为流式解码天然保证回滚。

同一 Decoder 在数组与连续值模式下的静态组成
图2:解码职责结构图。readArray 与 readSequence 是按协议选择的两种模式,图示不代表代码已执行。

怎样分辨是在等 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;网络连接读取截止时间。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go multipart 文件名为什么不能直接信任Go multipart 文件名为什么不能直接信任
上一篇
Go multipart 文件名为什么不能直接信任
Go bytes.Equal 和字符串比较的边界是什么
下一篇
Go bytes.Equal 和字符串比较的边界是什么
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    599次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    640次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    672次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    638次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    458次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码