--- tags: [后端, Go, Gin, 文件上传, 表单] create time: 2026-04-28 00:00 --- # 文件上传 ## 概述 文件上传在 Gin 中通过 `multipart/form-data` 实现——支持单文件和多文件上传、大小限制、类型校验,以及与业务逻辑的结合。理解底层 `MaxMultipartMemory` 行为是避免 OOM 的关键。 思考题:Gin 默认把超过内存阈值的临时文件存在哪里?这在容器化部署中会带来什么问题?(详见第 3 节) ## 正文 ### 1. 单文件上传 ```go 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` 签名:** ```go func (c *Context) SaveUploadedFile(file *multipart.File, dst string, maxBytes int64, matchers ...mimeType.Matcher) error ``` - `maxBytes`:文件大小限制(0 = 不限制,默认 8MB) - `matchers`:可选的文件类型校验函数 ### 2. 多文件上传 ```go 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()) ``` ```go r := gin.Default() // 调大内存阈值(谨慎使用) r.MaxMultipartMemory = 32 << 20 // 32MB srv := &http.Server{ Addr: ":8080", Handler: r, MaxHeaderBytes: 1 << 20, // 头部最大 1MB } ``` **容器化环境的陷阱:** ```mermaid 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),防止恶意文件伪装: ```go 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. 服务端合并分片为最终文件 ``` ```go 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]] — 上传后的文件如何通过静态文件服务对外暴露 - [[部署与运维基础]] — 容器环境下的磁盘和内存限制