gin 默认会卡住或报400错误,是因为其32mb内存限制触发http: request body too large拒绝;需调大maxmultipartmemory并改用c.request.multipartreader()流式处理避免oom。

大文件上传时为什么 Gin 默认会卡住或报 400 错误
Gin 默认限制了请求体大小(maxMemory 和底层 http.MaxBytesReader),超过默认 32MB 就会直接拒绝,返回 400 Bad Request 或静默截断。这不是 Gin 的 bug,而是 Go 标准库对内存安全的保守设计——它会把整个 multipart body 全部读进内存再解析,大文件(比如 500MB 视频)直接 OOM。
- 关键参数是
gin.Default() → gin.New()后需手动配置MaxMultipartMemory - 但仅调大
MaxMultipartMemory不够:它只控制内存缓冲区大小,不解决磁盘写入、流式处理、超时等问题 - 真正要直传(即不落地中间内存、边收边写),必须绕过 Gin 的
c.FormFile(),改用c.Request.MultipartReader()手动流式读取
如何用 MultipartReader 实现边收边写到磁盘
绕过 c.FormFile() 是必须的。它内部调用 ParseMultipartForm,会一次性把所有 part 加载进内存或临时文件,失去“直传”意义。正确做法是用 c.Request.MultipartReader() 获取原始 reader,逐 part 处理。
- 先调
c.Request.ParseMultipartForm(32 仅解析头部(非必须,但能提前校验 boundary 和字段名) - 再用
mr, err := c.Request.MultipartReader()获取流式 reader - 循环
mr.NextPart(),对目标 file part 调io.Copy(dstFile, part),避免part.ReadAll() - 注意:
part.Header.Get("Content-Disposition")里可能含 filename,需自己解析(标准 RFC 2388 格式)
mr, err := c.Request.MultipartReader()
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "invalid multipart"})
return
}
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": err.Error()})
return
}
if part.FormName() == "file" {
dst, _ := os.Create("/tmp/uploaded_" + filepath.Base(part.FileName()))
_, _ = io.Copy(dst, part) // 关键:流式写入
dst.Close()
}
}
超时、进度反馈和客户端配合要点
HTTP 协议本身不支持上传进度,所谓“进度条”必须靠客户端分块上传(如 tus、AWS S3 presigned POST)或服务端记录 offset。Gin 原生不提供上传中间状态,强行做进度需额外存储(Redis/DB)+ 长轮询/WebSocket,复杂度陡增。
- 务必设置
gin.SetMode(gin.ReleaseMode),否则调试模式下日志会拖慢大文件上传 - 调整 HTTP server 超时:
srv := &http.Server{ReadTimeout: 30 * time.Minute, WriteTimeout: 30 * time.Minute} - 客户端必须用
multipart/form-data,且不要设enctype以外的编码;filename 必须在Content-Disposition中显式声明 - 若需断点续传,Gin 无法原生支持——得换 tusd 或自己实现基于 chunk + offset 的协议
生产环境必须加的防护项
直传接口是攻击面高危入口。不加校验,用户可上传任意文件类型、超大体积、恶意 payload,甚至触发路径遍历(../../../etc/passwd)。
- 校验
part.FileName():过滤空值、路径遍历字符(..、/)、危险扩展名(.sh、.php) - 限制单个 part 大小:在
NextPart()后立刻用io.LimitReader(part, maxFileSize)包裹 - 生成唯一文件名:别信客户端传的
filename,用uuid.New().String()+ 安全后缀 - 写入前检查磁盘空间:
syscall.Statfs或disk.Usage(github.com/shirou/gopsutil)
真正难的不是代码写几行,而是边界条件:网络中断时文件残留怎么清理、并发上传冲突怎么加锁、失败后客户端重试是否幂等——这些没写进 demo,但线上跑一天就会暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











