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

Go json.Decoder.Buffered 怎样把预读字节交还给后续协议

来源:17golang原创

时间:2026-10-13 01:16:08 370浏览 收藏

Go 的 json.Decoder 会自己做缓冲,调用一次 Decode 时可能顺手从底层 Reader 多读一些字节。JSON 握手完成后,真正属于后续文本协议的数据可能已经躺在 Decoder 的内部缓冲里。处理方式是:在下一次调用 Decoder 前读取 Buffered(),把这段前缀保存下来,再用 io.MultiReader 按“前缀优先、原 Reader 其次”的顺序交给后续协议。

要点速览
  • Buffered() 返回的是 Decoder 已从输入读取、但尚未被 Decode 或 Token 消费的字节。
  • 返回的 Reader 只保证在下一次 Decode 或 Token 前有效,不能把它直接留给异步代码长期使用。
  • 最稳妥的交接方式是先复制缓冲内容,再用 io.MultiReader(bytes.NewReader(prefix), source) 合并读取。

先把 Decoder 的预读边界说清楚

json.NewDecoder 的输入边界不是一次底层 Read 调用的边界。它为了完成当前 JSON 值,可能已经读过后面的内容,因此原始 Reader 的当前位置不能代表“握手刚好结束”。Buffered() 返回剩余未消费数据,里面甚至可能还有尚未按 JSON 语法校验的字节;具体预读多少是实现细节,不能写死成某个长度。

还有一个容易忽略的生命周期:这个 Reader 只在下一次 Decode 或 Token 前有效。若握手成功后还要把读取工作交给另一个 goroutine,先把它复制出来。

用前缀缓冲把两个协议接起来

下面的例子把一条 JSON 握手和后面的文本行放在同一个输入流中。示例只演示 Reader 交接,不代表已经建立真实网络连接。

package main

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

func main() {
    // JSON 握手后紧跟后续协议数据,底层 Reader 不知道协议切换点。
    source := strings.NewReader(`{"op":"hello"}GET /next HTTP/1.1\r\n`)
    dec := json.NewDecoder(source)

    var hello struct {
        Op string `json:"op"`
    }
    // 先消费握手;错误必须在交接前返回,避免把半个握手交给下一层。
    if err := dec.Decode(&hello); err != nil {
        panic(err)
    }

    // 立刻复制预读内容,避免下一次 Decode/Token 让 Buffered Reader 失效。
    prefix, err := io.ReadAll(dec.Buffered())
    if err != nil {
        panic(err)
    }

    // 后续协议先读 prefix,再读原始输入中尚未被 Decoder 取走的部分。
    handoff := io.MultiReader(bytes.NewReader(prefix), source)
    rest, err := io.ReadAll(handoff)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s: %s\n", hello.Op, rest)
}

这里的关键不是把数据“塞回”原 Reader,而是给下一层构造一个新的读取视图。若 prefix 为空,io.MultiReader 仍然安全;若后续协议是持续流式读取,则应把 handoff 保留给它,而不是一次性调用 io.ReadAll。

Go json.Decoder Decode 后 Buffered 预读字节与原始 Reader 组成前缀优先读取链的结构说明图
图1:结构说明图,JSON Decoder 的 Buffered 前缀先被消费,再回到原始 Reader。

io.MultiReader 适合交接,但不能掩盖错误

解释 bytes.NewReader(prefix) 与 source 组成 io.MultiReader 后的顺序、错误出口和 net.Conn 边界。
图2:关系说明图,MultiReader 串联前缀与底层输入,错误仍需回传给调用方。

交接层可以抽成一个小函数,统一处理复制和读取错误。不要直接返回 dec.Buffered(),也不要在复制前再次调用 Decoder。对 TCP、管道这类可能长期阻塞的输入,后续层应该按自己的协议边界读取,不能用一次 ReadAll 等待流结束。

func readerAfterJSON(dec *json.Decoder, source io.Reader) (io.Reader, error) {
    // Buffered 只在下一次 Decode 或 Token 前有效,所以立即物化成独立字节切片。
    prefix, err := io.ReadAll(dec.Buffered())
    if err != nil {
        return nil, fmt.Errorf("读取 JSON 预读缓冲失败: %w", err)
    }
    // MultiReader 保证剩余前缀不丢失,再继续消费底层 Reader。
    return io.MultiReader(bytes.NewReader(prefix), source), nil
}

函数返回后,调用方可以把 Reader 交给按行协议、长度前缀协议或自定义解析器。若后续代码强制要求 net.Conn,不能简单地把 io.MultiReader 强转成连接;应让协议层接受 io.Reader,或实现一个包装连接,把 Read 做成“前缀优先”,同时转发原连接的 Write、截止时间和关闭方法。

场景推荐做法需要注意
同一流继续读取复制 Buffered 后使用 MultiReader前缀必须先读
异步交接复制为独立字节切片再传递不要保存 Buffered 返回值
后续接口要求 net.Conn调整接口或实现连接包装器保留 Write、Deadline、Close 语义

三个边界决定方案是否可靠

第一,JSON 握手只成功了一半时不要交接;Decode 返回错误就应该关闭或回收当前连接。第二,Buffered 的内容可能包含后续协议的任意字节,不能把它再次交给 JSON Decoder 做校验。第三,复制动作本身也可能返回错误,尤其是自定义 Reader;这个错误必须保留,不能为了继续读取而静默忽略。

如果协议设计允许,最好在握手阶段就约定明确的长度、分帧或连接升级规则。Buffered 解决的是“Decoder 已经多读了”的读取顺序问题,不会替你定义两个协议之间的安全边界。

常见问题

为什么直接把原始连接交给下一层会卡住

因为后续数据可能已经被 Decoder 读进自己的缓冲,原连接暂时没有这些字节。下一层只读原连接,就会等待永远不会再次到来的数据。

能不能多次调用 Buffered

可以读取当前返回的 Reader,但不要把它当作稳定快照。下一次 Decode 或 Token 后它就不再受支持;需要跨调用、跨 goroutine 使用时先复制。

Buffered 会返回完整的下一个消息吗

不保证。它只返回当前已经被底层 Reader 取来、却尚未被 Decoder 消费的部分,可能是完整消息、半条消息,也可能混有尚未验证的字节。后续协议仍要按自身边界继续读取。

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