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

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

来源:17golang原创

时间:2026-10-23 03:05:18 273浏览 收藏

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>