5.8 KiB
5.8 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-04-28 00:00 |
文件上传
概述
文件上传在 Gin 中通过 multipart/form-data 实现——支持单文件和多文件上传、大小限制、类型校验,以及与业务逻辑的结合。理解底层 MaxMultipartMemory 行为是避免 OOM 的关键。
思考题:Gin 默认把超过内存阈值的临时文件存在哪里?这在容器化部署中会带来什么问题?(详见第 3 节)
正文
1. 单文件上传
func uploadSingle(c *gin.Context) {
// 获取表单中的文件(最大 8MB 默认)
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
defer file.Close()
// 获取文件名
filename := header.Filename
// 保存到新位置
dst := filepath.Join("./uploads", filename)
if err := c.SaveUploadedFile(file, dst, 8<<20, nil); err != nil {
c.JSON(500, gin.H{"error": "save failed"})
return
}
c.JSON(200, gin.H{
"filename": filename,
"size": header.Size,
})
}
SaveUploadedFile 签名:
func (c *Context) SaveUploadedFile(file *multipart.File, dst string, maxBytes int64, matchers ...mimeType.Matcher) error
maxBytes:文件大小限制(0 = 不限制,默认 8MB)matchers:可选的文件类型校验函数
2. 多文件上传
func uploadMultiple(c *gin.Context) {
// 获取表单中的多个文件(最多 100 个)
form, _ := c.MultipartForm()
files := form.File["files"] // []*multipart.FileHeader
saved := make([]string, 0, len(files))
for _, file := range files {
filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)
if err := c.SaveUploadedFile(file, dst, 8<<20, nil); err != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to save %s", filename)})
return
}
saved = append(saved, filename)
}
c.JSON(200, gin.H{
"saved": saved,
"count": len(saved),
})
}
前端传参示例(multipart/form-data):
--boundary
Content-Disposition: form-data; name="files"; filename="photo1.jpg"
Content-Type: image/jpeg
[binary data]
--boundary
Content-Disposition: form-data; name="files"; filename="photo2.png"
Content-Type: image/png
[binary data]
--boundary--
关键区别: 单文件用
c.Request.FormFile("key");多文件用c.MultipartForm()拿到完整的*multipart.Form。两者操作的是同一个底层结构,只是访问粒度不同。
3. MaxMultipartMemory — 内存管理
Gin 底层继承自 http.Server,控制内存/磁盘切换的阈值是 MaxMultipartMemory(默认 8MB):
请求体大小 ≤ MaxMultipartMemory → 全部存内存
请求体大小 > MaxMultipartMemory → 超出部分写入临时文件(os.TempDir())
r := gin.Default()
// 调大内存阈值(谨慎使用)
r.MaxMultipartMemory = 32 << 20 // 32MB
srv := &http.Server{
Addr: ":8080",
Handler: r,
MaxHeaderBytes: 1 << 20, // 头部最大 1MB
}
容器化环境的陷阱:
flowchart LR
A["上传 50MB 文件"] --> B{"≤ 8MB?"}
B -->|"否"| C["超出部分写入 /tmp"]
C --> D["容器 /tmp 空间有限"]
D --> E["磁盘满 → 服务崩溃"]
style E fill:#ffebee,stroke:#c62828
最佳实践:
- 不要过度放大
MaxMultipartMemory——用流式处理代替全量加载 - 容器环境中监控
/tmp用量 - 考虑使用对象存储(S3/OSS)直传,服务端只生成预签名 URL
4. 文件类型与大小校验
仅靠大小限制不够,还需要校验实际文件内容类型(MimeType),防止恶意文件伪装:
import "golang.org/x/exp/mime/multipart" // or use net/textproto
func validateFile(file multipart.File) error {
// 读取前 512 字节检测真实 MIME 类型
buf := make([]byte, 512)
_, err := file.Read(buf)
if err != nil {
return err
}
contentType := http.DetectContentType(buf)
// 只允许图片
allowed := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/webp": true,
}
if !allowed[contentType] {
return fmt.Errorf("unsupported file type: %s", contentType)
}
// 重置读取位置以便后续保存
file = multipart.NopCloser(bytes.NewReader(buf))
_ = file
return nil
}
安全提醒: 永远不要用文件扩展名判断类型(如
.jpg)。攻击者可以轻松绕过——应该基于文件内容的 magic bytes(魔数)检测。
5. 分片上传思路
超大文件(GB 级别)不适合直接上传,应使用分片上传:
流程:
1. 客户端将大文件切分为 N 个小片(如每片 5MB)
2. 依次上传每个分片 → POST /upload/chunk?fileName=x&chunkIndex=0
3. 服务端将分片暂存到临时目录
4. 所有分片上传完成后,POST /upload/merge?fileName=x
5. 服务端合并分片为最终文件
func uploadChunk(c *gin.Context) {
fileName := c.Query("fileName")
chunkIndex := c.Query("chunkIndex")
totalChunks := c.Query("totalChunks")
file, _ := c.FormFile("chunk")
dst := filepath.Join("./tmp-chunks", fileName, chunkIndex)
os.MkdirAll(filepath.Dir(dst), 0755)
c.SaveUploadedFile(file, dst, 10<<20, nil)
// 检查是否最后一个分片
if chunkIndex == totalChunks {
mergeChunks(fileName, int64(totalChunks))
}
c.JSON(200, gin.H{"status": "chunk uploaded"})
}
关联笔记
- GIN/7-binding-advanced — multipart/form-data 是表单绑定的一个特例
- GIN/11-static-files — 上传后的文件如何通过静态文件服务对外暴露
- 部署与运维基础 — 容器环境下的磁盘和内存限制