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

Go 怎样限制上传正文大小

来源:17golang原创

时间:2026-10-21 23:38:49 312浏览 收藏

Go 的上传接口应在首次读取请求正文之前,用 http.MaxBytesReader(w, r.Body, limit) 替换 r.Body,再读取或解析它。读取返回 *http.MaxBytesError 时,由处理器明确回复 HTTP 413;不要只检查 Content-Length,也不要把 ParseMultipartForm 的内存参数当作上传总大小限制。

以一个“接收二进制正文并报告字节数”的接口为例:业务规定整条正文不超过 2 MiB。调用方发送多大的文件是一回事,服务端实际读取了多少字节是另一回事。下面把判断落实在读取入口,读完成功后才返回接收结果。

Go 官方地址:https://go.dev/
net/http 官方文档:https://pkg.go.dev/net/http

正文上限应该加在哪里

上限应覆盖真正读取 Body 的代码。假如中间件已调用 ReadAll、JSON 绑定或表单解析,随后在业务 Handler 才包装 Body,之前的读取就已经发生。更合适的顺序是先通过路由确定限额,再包装 Body,最后让后续解析器使用这个包装后的入口。

这里的限制是请求正文,不是整个网络请求,也不包含 HTTP 请求头。对于原始二进制接口,正文通常就是文件内容;对于 multipart,正文还包含各部分的头、分隔边界和普通字段,因此“文件最多 10 MiB”不能直接等同于“整个表单最多 10 MiB”。

MaxBytesReader 返回可关闭的读取器,读取超过限制时用特定错误表示;调用 Close 也会关闭底层读取器。它限制的是消费者能够从这个入口读入的内容,不是宣称超出的字节绝不会抵达服务器网络缓冲。MaxBytesReader 文档

Request.Body由MaxBytesReader包装,maxBodyBytes定义限额,读取器供io.Copy使用
图1:正文读取边界结构说明图。“请求入口”和“读取约束”区分被包装的正文、限额和消费者;连线表示依赖关系,不是执行时间轴或截图。

只看ContentLength为什么还不够

r.ContentLength 可以用于已知大请求的快速拒绝,但有些请求没有预先确定的正文长度。代码只写“ContentLength 大于上限就拒绝”,不能表达实际读取过程的限制。因此示例保留这个提前判断,同时仍包装 Body。

io.LimitReader 也不等同于“超限时报错”:它达到自己的限制后表现为 EOF,调用方如果据此当作完整上传成功,就可能接受被截断的数据。MaxBytesReader 会在试图读过限制时给出可辨识的错误。其实现读取时会为判断是否超出边界留出探测空间,因此恰好等于上限并正常结束的正文可以成功,超过上限则应拒绝。请求正文限制实现

另一个易漏点是只读开头就返回成功。包装器不会凭空读取剩余内容;如果应用只消费一小段而从未读到超限位置,就不能声称已经确认整条上传符合限额。本例使用 io.Copy 读到结束,再判断错误,避免这个含混。

一个能明确返回413的处理器

以下代码面向 Go 1.19 及以上,使用 errors.As 识别 MaxBytesError。它是正文计数示例,不把上传落盘,也不接受压缩解码职责;网络超时值为演示配置,并非所有上传场景都适用。

package main

import (
    "errors"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

const maxBodyBytes int64 = 2  maxBodyBytes {
        http.Error(w, "上传正文超过 2 MiB", http.StatusRequestEntityTooLarge)
        return
    }

    // 只计数不保存文件,避免给示例引入持久化副作用。
    n, err := io.Copy(io.Discard, r.Body)
    if err != nil {
        var tooLarge *http.MaxBytesError
        if errors.As(err, &tooLarge) {
            http.Error(w, "上传正文超过 2 MiB", http.StatusRequestEntityTooLarge)
            return
        }
        http.Error(w, "读取上传正文失败", http.StatusBadRequest)
        return
    }

    // 读到 EOF 且没有错误以后才确认接收成功。
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    fmt.Fprintf(w, "接收正文 %d 字节\n", n)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/upload", inspectUpload)
    server := &http.Server{
        Addr: "127.0.0.1:8099", // 仅供本机练习,不直接对外暴露。
        Handler: mux,
        ReadHeaderTimeout: 5 * time.Second,
        ReadTimeout: 20 * time.Second,
        WriteTimeout: 25 * time.Second,
        IdleTimeout: 60 * time.Second,
    }
    // 时间参数只是演示值,生产环境按上传协议和网络条件配置。
    log.Fatal(server.ListenAndServe())
}

io.Copy 返回的 n 只是已经复制的字节数,不能脱离 err 单独代表完整上传。在错误路径,本例先判断是否属于体积超限,再处理普通读取失败;成功路径不复用部分结果。这样调用方可以区分“正文超过配额”和“请求未能完整读取”。

不要通过匹配错误文本来识别超限,也不要先写出 200 状态再处理 Body 的错误。HTTP 响应已经提交后,处理器无法把它改成原本应该返回的 413。若实际写临时文件,应在全部读取和业务检查成功后再提交;失败时删除未完成文件,不以部分数据作为正式产物。

表单上传的内存参数是不是文件上限

不是。ParseMultipartForm(maxMemory) 解析 multipart/form-data,把文件部分的一定份额保留在内存,剩余内容可能存为临时文件。内存参数不是“超过就拒绝”的单文件规则,也不是整个 HTTP 正文上限。普通字段与解析开销同样需要考虑,不能把这个数理解为进程内存总预算。ParseMultipartForm 文档

如果接口改为表单上传,可以把完整示例中的读取段换成下面的处理方式;它是另一种读取方案,不要先运行 io.Copy 把 Body 消耗完,再执行这段解析。

// 片段位于 Handler 内,w 和 r 为处理器参数。
r.Body = http.MaxBytesReader(w, r.Body, 12

这里 12 MiB 是包含字段和分隔符的总正文配额,1 MiB 是文件部分的内存阈值,两者各司其职。解析产生的临时文件应在不再需要时清理。若通过 FileHeader.Open 打开了文件,使用完成后仍需关闭相应文件句柄;RemoveAll 不是替代 Close 的接口。multipart 临时文件清理

MaxBytesReader关联总正文配额,ParseMultipartForm关联内存阈值与MultipartForm,RemoveAll负责临时文件清理
图2:表单配额与资源职责说明图。两个分组区分“接收配额”和“表单资源”;连线只说明配置与资源关联,不表示执行步骤或内存实测结果。

怎样判断限制确实覆盖了接口

把完整示例保存为 main.go,在练习目录运行以下命令。本文不启动服务器或执行测试,以下仅供读者在本地复现。

# 首次在练习目录创建模块;已有 go.mod 时不重复初始化。
go mod init example.com/body-limit-demo
go run .

再在另一终端用一个很短的正文发请求:

# 原始正文为三个 ASCII 字节,应返回 HTTP 200 和计数字样。
curl -i --data-binary 'abc' http://127.0.0.1:8099/upload

按代码应返回“接收正文 3 字节”。这是预期结果,不是实际运行记录。边界输入应按下表设计,特别不要只测已知 Content-Length 的大文件。

读者复现场景本例预期对应判断
3 字节原始正文200,计数为 3正常完整读取
恰好 2 MiB 且正常 EOF200上限允许等值
2 MiB 加一个字节413已知长度快速拒绝或读取超限
未知长度且实际超限413不能依赖 ContentLength
正文读取中断不进入成功分支连接状态可能令错误响应无法送达

压缩正文、多文件和慢上传还要管什么

压缩正文按哪个大小算?按包装器所在层读取的字节算。如果先限制压缩数据、再交给解压器,不能据此约束解压后的数据总量。需要同时定义编码正文和解码内容的不同上限;本文没有添加自动解压,也不提供压缩内容解析承诺。

总正文上限能替代单文件大小吗?不能。它能约束总体积,但业务若要求每个文件不超过固定大小,还要在明确的表单结构上分别检查文件、数量和字段。使用流式 multipart 逐个读取时,同样需要每部分的计数与失败清理策略;本例不把这套额外功能伪装成已实现。

字节上限能防止慢上传一直占用连接吗?不能单靠它。体积、时间和并发是不同约束,需要结合 HTTP Server 超时、代理入口限制与业务并发预算。前置代理如果先拒绝请求,调用方看到的 413 也可能来自代理,而非 Go 处理器。

这个接口的核心不是某个表单参数,而是把体积规则落实到真正的读取入口:先包装,再完整消费并处理错误,最后决定是否接受业务结果。总正文、单文件、内存阈值和读取时间分别定义,限制才不会名义存在、实际绕过。

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