Gin 本身不阻塞,但 c.FormFile() 和文件写入是同步的;必须在主协程完成表单解析并获取 *multipart.FileHeader 后,再用 goroutine 并发保存,且需处理路径安全、目录存在性及错误聚合。

直接结论:Gin 本身不阻塞,但 c.FormFile() 和文件写入操作是同步的;并发保存多个上传文件必须显式启动 goroutine,并隔离 *gin.Context 中的请求数据和临时资源。
为什么不能直接在 Handler 里用 go func() { c.FormFile() }?
因为 *gin.Context 不是 goroutine 安全的——它的内部字段(如 c.Request.MultipartForm)在主协程解析完表单后可能被复用或释放。一旦你在 goroutine 中延迟调用 c.FormFile() 或 c.SaveUploadedFile(),大概率会遇到:
-
panic: multipart: NextPart: EOF或http: request body closed - 读到空文件、截断内容,或 panic 报
nil pointer dereference - 多个 goroutine 竞争读取同一份
multipart.Form.File映射,结果不可预测
正确做法:先完成表单解析,再并发保存
核心原则是——所有对 c.Request 的读取(包括 c.FormFile())必须在主 goroutine 中一次性做完,拿到 *multipart.FileHeader 列表后,再把每个 FileHeader 和目标路径传给独立 goroutine。
示例代码关键步骤:
func uploadMulti(c *gin.Context) {
// 1. 必须先 ParseMultipartForm,否则 FormFile() 会失败
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
c.JSON(400, gin.H{"error": "parse form failed: " + err.Error()})
return
}
<pre class="brush:php;toolbar:false;">// 2. 提前获取所有文件头(不是打开文件!只是元信息)
form, _ := c.MultipartForm()
files := form.File["files"] // 假设前端用 name="files" 多选上传
// 3. 创建 channel 控制并发数(防 OOM)
ch := make(chan error, len(files))
sem := make(chan struct{}, 5) // 限制最多 5 个并发写入
for _, f := range files {
sem <- struct{}{} // 获取信号量
go func(fh *multipart.FileHeader, filename string) {
defer func() { <-sem }() // 释放信号量
dst := "./uploads/" + filename
if err := c.SaveUploadedFile(fh, dst); err != nil {
ch <- fmt.Errorf("save %s failed: %w", filename, err)
return
}
ch <- nil
}(f, f.Filename)
}
// 4. 等待全部完成
for i := 0; i < len(files); i++ {
if err := <-ch; err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
}
c.JSON(200, gin.H{"message": "all saved"})}
注意 c.SaveUploadedFile() 的隐含行为
这个方法本质是:fh.Open() → os.Create(dst) → io.Copy()。它会完整读取整个文件内容到磁盘,期间占用内存(缓冲区)和文件句柄。容易被忽略的关键点:
- 它不支持流式处理大文件(比如 >1GB),会吃光内存;真要处理大文件,请改用
fh.Open()+ 自定义io.Pipe或分块写入 - 目标目录(如
./uploads/)必须提前存在,c.SaveUploadedFile()不会自动创建父级路径 - 文件名来自客户端(
fh.Filename),直接拼接路径有路径遍历风险;务必用filepath.Base()过滤
更安全的生产写法:用 sync.WaitGroup + 错误聚合
比起 channel 收集错误,sync.WaitGroup 更直观,也避免 channel 缓冲区大小误判:
var wg sync.WaitGroup
var mu sync.Mutex
var errs []error
<p>for _, f := range files {
wg.Add(1)
go func(fh *multipart.FileHeader) {
defer wg.Done()
dst := filepath.Join("./uploads", filepath.Base(fh.Filename))
if err := c.SaveUploadedFile(fh, dst); err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
}(f)
}
wg.Wait()</p><p>if len(errs) > 0 {
c.JSON(500, gin.H{"errors": errs})
return
}真正复杂的地方不在“开 goroutine”,而在于上下文生命周期管理、文件系统资源竞争、以及客户端可控输入带来的路径安全。这些细节一旦漏掉,服务上线后就会在高并发上传时静默失败或被攻击。


















