vault backup: 2026-04-28 08:53:28

This commit is contained in:
2026-04-28 08:53:28 +08:00
parent 2df3748fc9
commit 8b07798991
23 changed files with 3349 additions and 39 deletions
+214
View File
@@ -0,0 +1,214 @@
---
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]] — 上传后的文件如何通过静态文件服务对外暴露
- [[部署与运维基础]] — 容器环境下的磁盘和内存限制