必须调用 parsemultipartform 设置合理内存阈值(如32mb),否则未解析就调 formfile 会报错“no such file”,阈值过小则频繁磁盘io拖慢响应。

Go 里怎么用 multipart/form-data 接收分块文件
直接读 r.Body 会吃光内存,大文件上传必须靠 ParseMultipartForm 拆解。Go 默认只缓存 32MB 到内存,超了就写临时文件——但这个阈值得手动设,否则小块上传也可能触发磁盘 IO,拖慢响应。
常见错误是没调 ParseMultipartForm 就直接 r.FormFile,结果返回 http: no such file;或者设了超小阈值(比如 1KB),导致每个分块都落盘,IO 翻倍。
- 必须在
r.ParseMultipartForm(32 显式设上限,单位字节,建议至少 <code>32 (32MB) - 分块字段名统一用
file,后端用r.FormValue("chunkIndex")、r.FormValue("totalChunks")拿元数据 - 别依赖
r.MultipartForm.File的 map 顺序,它不保证和前端发送顺序一致
分块合并时如何避免竞态和磁盘爆满
多个请求并发写同一个文件的临时分块,不加控制就会丢数据。更危险的是:用户中断上传后,残留在 /tmp 的分块没人清理,几天就能占满磁盘。
不能靠文件名哈希做唯一标识——不同用户传同名文件会冲突;也不能等所有块收齐再合并——万一最后一块永远不来,临时目录就积压着。
立即学习“go语言免费学习笔记(深入)”;
- 每个上传会话用 UUID 生成独立临时目录,路径如
/tmp/upload_abc123/ - 每块写入前先
os.OpenFile(..., os.O_CREATE|os.O_EXCL),失败说明已存在,跳过或报错 - 用
filepath.Glob("/tmp/upload_*/*")定期扫 24 小时未更新的块,配合os.Chtimes记最后写入时间
io.Copy 合并分块为什么比 os.Write 更稳
用 os.Write 循环写分块容易出错:偏移算错、字节数不匹配、没检查 n, err := w.Write(buf) 的 n 是否等于 len(buf),结果文件中间缺一段。
io.Copy 自动处理 buffer 复用和 partial write,但要注意源必须是 seekable(比如 *os.File),否则合并时会从头重读——你得确保每个分块文件都支持 Seek(0, io.SeekStart)。
- 合并前对每个分块文件
f, _ := os.Open(path); _, _ = f.Seek(0, io.SeekStart)确保可重读 - 目标文件用
os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0644),别用O_TRUNC,防止合并中途崩溃清空已有内容 - 合并完立刻
os.Chmod(dst, 0644),避免权限问题导致后续无法访问
断点续传怎么判断“这块我收过了”
前端传 chunkIndex=5,服务端不能只查文件是否存在——因为上一次上传可能写了一半就中断,文件大小不对,直接跳过会导致最终文件损坏。
最可靠的方式是比对分块内容哈希(如 sha256),但计算哈希又耗 CPU。折中方案是记录每个块的 size + modtime,只要前后两次上传同索引块大小和修改时间一致,就认为内容相同。
- 接收每块后立即
stat, _ := f.Stat(),存{chunkIndex: {size: stat.Size(), mtime: stat.ModTime()}}到内存 map 或 Redis - Redis key 建议用
upload:{uuid}:chunks,field 是5,value 是{"size":1048576,"mtime":"2024-06-10T14:22:33Z"} - 如果前端带了
Content-MD5header,优先校验它,比 size+mtime 更准
真正难的不是逻辑,是上传 ID 的生命周期管理——UUID 怎么生成、存哪、过期多久、谁来删。这些不和业务存储层对齐,断点续传就是纸糊的。










