中间件不适合做分片合并后的临时文件清理——因其仅作用于单次请求生命周期,无法处理异步、跨请求、失败重试等场景,且无法感知合并函数内部错误、安全访问动态路径或避免并发竞态;清理必须内聚于合并函数中,用defer或显式分支控制。

为什么中间件不适合做分片合并后的临时文件清理
中间件不是清理临时文件的合适位置——它只负责请求生命周期内的逻辑,而分片合并是异步、跨请求、可能失败后重试的操作。一旦合并失败或校验不通过,你得立刻删掉目标文件和所有分片;但此时 handler 已返回响应,中间件早已退出,无法触发清理动作。
更危险的是:若把清理逻辑塞进某个“合并完成”接口的中间件里,会误判成功时机。比如 io.Copy 写完但 sha256.Sum256 校验失败,中间件看不到这个失败,就跳过清理,留下脏数据。
- 中间件无法感知合并函数内部的错误分支(如校验失败、写入中断)
- 它不能安全访问分片目录路径——这些路径通常由上传时的
identifier动态生成,不在上下文公共字段中 - 并发上传同一文件时,多个请求可能同时尝试清理同一目录,引发 race condition
临时文件清理必须放在合并函数内部
真正安全的清理,必须和合并、校验耦合在同一个函数作用域里,用 defer 或显式 if 分支控制。失败时立刻删,成功时按需保留(比如留 24 小时供断点续传)。
示例结构如下:
func mergeChunks(identifier string, dstPath string) error {
tmpDir := filepath.Join("/tmp/uploads", identifier)
defer os.RemoveAll(tmpDir) // 先挂起,后面可能提前 return
// 排序分片
files, err := filepath.Glob(filepath.Join(tmpDir, "part_*"))
if err != nil {
return err
}
sort.Slice(files, func(i, j int) bool {
return extractNum(files[i]) < extractNum(files[j])
})
// 创建目标文件
dst, err := os.Create(dstPath)
if err != nil {
return err
}
defer dst.Close()
// 流式合并
for _, f := range files {
src, err := os.Open(f)
if err != nil {
return err
}
_, err = io.Copy(dst, src)
src.Close()
if err != nil {
return err // 此处 return 会触发 defer os.RemoveAll(tmpDir)
}
}
// 校验
expectedHash := getExpectedHash(identifier) // 从上传时存的 meta 取
actualHash := calcFileSHA256(dstPath)
if !bytes.Equal(expectedHash[:], actualHash[:]) {
os.Remove(dstPath)
return errors.New("sha256 mismatch")
}
return nil // 成功,defer 已删 tmpDir
}
如何避免清理误删正在上传的分片
关键不是“什么时候删”,而是“删谁”。不能无差别 os.RemoveAll("/tmp/uploads"),必须严格绑定到 identifier 目录。否则一个上传失败的清理动作,可能干掉另一个还在进行中的上传任务。
- 每个分片写入前,用
os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL)确保不会覆盖已有分片 - 合并函数只操作
/tmp/uploads/{identifier}/,绝不碰父级 - 若需长期保留分片(如支持断点续传),不要删目录,改用
os.Chtimes更新 mtime,再由后台定时任务扫描过期目录 - 禁止在 handler 中直接调
c.Request.MultipartReader()后又让中间件去清理——中间件拿不到identifier,根本不知道删哪
清理失败比不清理更危险
如果 os.RemoveAll(tmpDir) 返回 error(比如权限不足、文件被占用),你不能忽略它。此时 tmpDir 里残留了未清理的分片,下次同名上传会覆盖失败,或校验错位。
正确做法是记录错误并报警,而不是静默吞掉:
- 用
os.RemoveAll后检查 err,非nil且非os.IsNotExist时,写日志 + 上报监控 - 不要用
os.RemoveAll做“尽力而为”清理——它要么全删干净,要么明确失败 - 生产环境建议用
filepath.WalkDir配合os.Remove逐个删,并对每个失败项单独处理,避免一个文件卡住整个目录
最常被忽略的一点:清理逻辑写在合并函数里,不代表它就自动可靠。你得确保所有错误出口(包括 panic 恢复)都覆盖到,否则一次 I/O 中断就可能留下永久垃圾。


















