因为Gin默认的文件上传方法不感知连接中断,也不绑定context生命周期;客户端断开时临时文件不会自动回收,必须在中间件中监听context.Done()实时清理,且需同步调大Nginx超时参数并用MaxBytesReader二次限流才能可靠触发取消信号。

为什么断开连接后临时文件不自动清理
因为 Gin 默认的 c.SaveUploadedFile 或 c.FormFile 不感知连接中断,也不绑定 context 生命周期;一旦客户端断开(如网络闪断、浏览器关闭、超时),Gin handler 可能仍在执行或已 panic,但已创建的临时文件句柄和磁盘文件不会被自动回收。更危险的是:中间件里用 os.CreateTemp 打开文件后,若没显式 file.Close() 就 abort,fd 泄露 + 文件残留会持续累积。
必须在中间件中监听 context.Done() 并主动清理
不能等 handler 结束——它可能根本不会执行完。清理动作必须嵌入到接收分片的中间件逻辑里,在读取 body 过程中实时响应取消信号。
- 中间件开头调用
c.Request.ParseMultipartForm(0)禁用自动解析,避免提前消费c.Request.Body - 用
multipart.NewReader(c.Request.Body, boundary)获取 reader,再循环part := reader.NextPart() - 每次拿到文件 part 后,立即
tmpFile, err := os.CreateTemp("/tmp/uploads", "chunk_*.bin") - 启动 goroutine 监听
c.Request.Context().Done(),一旦触发就os.Remove(tmpFile.Name())并tmpFile.Close() - 主流程用
io.CopyN(tmpFile, part.Body, chunkSize)写入,写完立刻tmpFile.Close()并取消监听 goroutine
合并前还要防并发和状态漂移
即使单次上传清理了,合并阶段仍可能因重复请求或服务重启导致脏数据。Redis 中的 upload:{id}:parts 记录只是索引快照,不代表文件一定存在或完整。
- 合并函数
mergeChunks(identifier, dstPath)开头先检查所有分片文件是否真实存在且非空 - 用
os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)创建目标文件,防止竞态覆盖 - 追加写入每个分片后,用
os.Remove清理该分片,而不是等全部写完再统一删——避免中途失败导致残留 - 整个合并过程用 Redis 分布式锁(
SET upload:{id}:lock "1" EX 30 NX)保护,超时自动释放
最容易被忽略的点:Nginx 层断连不通知 Gin
Nginx 在 client_body_timeout 超时后直接断开连接并返回 408,但 Gin 的 c.Request.Context() 不会收到 cancel 信号——因为连接是 Nginx 主动关的,Go net/http 根本收不到 FIN。这意味着你监听 context.Done() 完全无效。
必须同步调大 Nginx 的 client_body_timeout 和 client_max_body_size,并在 Gin 层用 http.MaxBytesReader 包装 c.Request.Body 做二次限流,让超限行为在 Go 层可控地触发 cancel,才能真正捕获并清理。


















