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

jsonDecoder读完对象后还有内容怎么办

来源:17golang原创

时间:2026-11-15 07:00:09 169浏览 收藏

如果接口约定请求体只能包含一个 JSON 对象,第一次 json.Decoder.Decode 成功后,应使用同一个 Decoder 再尝试读取一个值:只有返回 io.EOF 才表示后面仅有空白并且输入已经结束。第二次返回 nil 表示还存在另一个完整 JSON 值,其他错误则可能表示尾部损坏、读取失败或超出大小限制,都不应按正常结束接受。

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

用输入矩阵建立基线

一个更新标题接口可能收到以下内容。先明确要求是“一个对象”,再比较只调用一次 Decode 和补充结束检查后的行为,才能判断当前实现漏掉了什么。

输入内容第一次 Decode单对象契约的预期结果
一个有效对象可以成功接受
对象后跟空格或换行可以成功接受
对象后再跟另一个对象可以只读到首对象拒绝
对象后跟 null可以只读到首对象拒绝
对象后跟损坏内容可能先成功拒绝

这些是协议与 API 语义对应的预期判断,没有声称在本机执行过测试。问题的基线是漏接收了多少种不符合协议的输入,而不是先假设需要替换 JSON 库或提高解析速度。

区分值边界与流边界

Decoder 面向流,每次 Decode 读取下一个 JSON 值。因此第一个对象结束,不意味着整个 Reader 结束。它还会自行缓冲输入,甚至提前读取超过当前 JSON 值的字节。若直接从原始 Reader 再读取,可能漏掉已经进入 Decoder 缓冲区的内容。

继续使用同一 Decoder,可以让结束检查覆盖它已缓存的部分和底层尚未读取的部分。Buffered 适合需要访问当前缓冲余量的协议场景,但只检查 Buffered 是否为空,也不能证明底层输入已经结束。

Reader与Decoder缓冲区和JSON值的结构关系
图1:输入边界结构说明图。Decoder 同时关联内部缓冲与底层 Reader,首个值的边界不等于输入结束;图中连线不是执行流程。

第二次 Decode 检查 EOF

下面代码实现一个简单 HTTP 处理器及其解析函数。JSON 对象采用导出字段 Title;示例输入可写为 {"Title":"新标题"}。这里只允许一个非 null 对象,并对未知字段报错,Title 必须非空。DecodeOne 不负责关闭传入 Reader,关闭请求体由处理器负责。

package api

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
)

type Update struct {
    Title string
}

// DecodeOne 在全部格式检查通过后才返回可供业务使用的数据。
func DecodeOne(r io.Reader) (Update, error) {
    dec := json.NewDecoder(r)
    dec.DisallowUnknownFields()

    // 指针目标用于拒绝顶层 null;数组、字符串等也不符合对象类型。
    var input *Update
    if err := dec.Decode(&input); err != nil {
        return Update{}, fmt.Errorf("读取首对象: %w", err)
    }
    if input == nil {
        return Update{}, errors.New("请求体必须为对象")
    }

    // RawMessage 接受任意第二个值,避免把第二个值的类型错误当成结束。
    var extra json.RawMessage
    err := dec.Decode(&extra)
    if err == nil {
        return Update{}, errors.New("请求体只能包含一个对象")
    }
    if !errors.Is(err, io.EOF) {
        return Update{}, fmt.Errorf("读取对象尾部: %w", err)
    }
    if input.Title == "" {
        return Update{}, errors.New("Title 不能为空")
    }
    return *input, nil
}

func UpdateHandler(w http.ResponseWriter, r *http.Request) {
    // 示例限制为 64 KiB,按真实业务选择上限;不要只信 Content-Length。
    r.Body = http.MaxBytesReader(w, r.Body, 64

把处理器注册到项目已有路由即可。示例没有实现持久化:204 只表示演示处理器接受了输入,不能当作数据库已更新。业务接入时应在注释指定位置执行操作并处理其错误,再返回成功。

错误判断必须使用 EOF,而不是把所有非 nil 错误都理解成“没有更多内容”。例如尾部只有一个未完成的对象,可能得到 unexpected EOF;它表示缺失输入,不能按正常 EOF 放行。第二个值即使是 null、数字或数组,也违反本例单对象约定,因此用 RawMessage 统一接收检测即可。

处理 HTTP 大小限制与业务提交

MaxBytesReader 在读取超过限额时报告错误,并不是凭 Content-Length 预先判断。第一次解码对象很小,但后面跟着大量空白时,第二次读取仍可能触发限额;错误应保留而不是吞掉。本例使用错误包装并通过 errors.As 识别 MaxBytesError。

简单的 io.LimitReader 会在限额处给出类似输入结束的效果,若只读允许字节数而不检查额外内容,可能把被截掉的尾部误认为不存在。因此 HTTP 场景优先使用能区分“真实 EOF”和“超出限额”的机制。大小限制也不能替代读超时:客户端一直不结束请求体时,结束检查可能继续等待,应由服务器读取超时策略约束。

单对象契约的错误分类和业务提交条件
图2:单对象契约结构说明图。EOF、额外值和尾部错误属于不同判定,大小限制贯穿输入读取;只有契约通过的数据才供业务使用。

业务提交位置尤其重要。若解码第一个对象就写数据库,之后才发现尾部无效,返回 400 也无法自动撤回已发生的动作。应先完成首对象、尾部和业务字段检查,再进入业务事务。

说明流式协议及测量方法

如果协议本来允许多个 JSON 值,就应循环 Decode 直到正常 EOF,而不是采用本例的“第二个值即拒绝”规则。NDJSON 还可能要求一行一条记录,需要另外处理行边界。Decoder.More 用于数组或对象内的元素判断,不能替代顶层 Reader 是否结束的检查。

单对象尾部检查并不检查所有协议细节。DisallowUnknownFields 不会自动禁止重复键,也不会强制字段大小写精确匹配;这些要求应单独设计。此处重点是对象之后的剩余输入,不应把它称为全面严格 JSON 校验器。

评估改动时,先对表中各类输入分别测试接受或拒绝,再用相同工具链、对象大小、尾随空白长度和输入比例比较耗时与分配。记录 ns/op、B/op 和 allocs/op 时,应把正常对象、额外对象、损坏尾部拆开观察。第二次读取可能增加扫描成本,也可能等待底层输入结束;没有实测数据就不应承诺吞吐提升。本文未执行压测,不提供虚构结果。

可以改用 json.Unmarshal 吗?

若已经有完整且受限的字节切片,Unmarshal 会要求输入构成一个完整 JSON 值并允许尾随空白。但若先从网络无限量 ReadAll,再调用它,仍没有解决输入大小和等待问题。

第二次 Decode 会修改首对象吗?

示例把第二次结果放进独立 RawMessage,不复用业务对象;无论尾部结果如何,失败都返回空业务值。

尾部只有换行为什么还需要读取?

必须读过这些合法空白并确认真实输入结束,才能判断后面没有第二个值或无效内容。

官方参考:https://pkg.go.dev/encoding/json#Decoder.Decode;请求体限额:https://pkg.go.dev/net/http#MaxBytesReader。

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