分片上传前必须处理内存溢出、分片大小动态计算、请求头缺失校验三大边界问题;需动态计算分片大小、校验X-Upload-ID与X-Part-Number、用io.LimitReader截断、追加写入临时文件并sync、合并时用io.MultiReader流式处理。

分片上传前必须处理的三个边界问题
Go 语言里直接用 http.Request.Body 读取大文件会吃光内存,必须边读边分片;分片大小不能硬编码为固定值(比如 5MB),要根据客户端实际上传的 Content-Length 和服务端内存限制动态计算;HTTP 请求头中若缺失 Content-Range 或 X-Upload-ID,应拒绝该分片而非静默忽略。
- 建议分片大小设为
min(8 * 1024 * 1024, totalSize/10),上限 8MB,下限 512KB,避免小文件切出过多碎片 - 每个分片必须携带唯一
X-Upload-ID(如 UUIDv4)和当前序号X-Part-Number,服务端靠这两个字段做幂等写入 - 不要信任客户端传来的
Content-Length,要用io.LimitReader严格截断,防止恶意超长分片绕过校验
用 os.OpenFile 追加写入分片文件
分片不是存进内存再拼,而是直接落盘。用 os.O_CREATE | os.O_WRONLY | os.O_APPEND 模式打开临时文件,确保并发上传时多个 goroutine 写同一文件不覆盖——系统级追加写是原子的,比自己加锁更可靠。
file, err := os.OpenFile(fmt.Sprintf("/tmp/upload_%s.part", uploadID), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, r.Body) // r.Body 是当前分片的 reader
- 临时文件路径必须包含
uploadID,避免不同上传任务互相污染 - 别用
os.WriteFile,它会覆盖整个文件;也别用os.Seek + Write模拟随机写,容易在并发下错位 - 写完立刻调用
file.Sync(),防止 OS 缓存未刷盘导致合并时读到空内容
合并时用 io.MultiReader 避免内存爆炸
合并不是把所有分片 os.ReadFile 进内存再 os.WriteFile,那样 2GB 文件会申请 2GB 内存。正确做法是构造一个按序读取各分片的 reader,再流式写入目标文件。
parts := make([]io.Reader, partCount)
for i := 1; i <= partCount; i++ {
f, _ := os.Open(fmt.Sprintf("/tmp/upload_%s_%d.part", uploadID, i))
parts[i-1] = f
}
mr := io.MultiReader(parts...)
dst, _ := os.Create(finalPath)
io.Copy(dst, mr) // 流式合并,内存占用恒定 ~64KB
- 务必按
X-Part-Number升序组装parts切片,乱序会导致文件损坏 - 合并前检查每个分片文件是否存在且大小匹配预期(比如第
n片应为min(chunkSize, remaining)),缺片直接返回 400 - 合并完成后立即删除所有
.part文件,用os.RemoveAll而非循环os.Remove,避免删漏
SHA256 校验必须在合并后、落盘前完成
校验不能对每个分片单独算 SHA256 再拼摘要——那等于没校验。必须对最终合并出的完整文件流重新计算一次 SHA256,并与客户端预提交的 X-Expected-SHA256 对比。否则中间网络篡改或磁盘静默错误无法发现。
立即学习“go语言免费学习笔记(深入)”;
- 用
io.TeeReader在io.Copy合并的同时计算哈希,避免二次读盘开销 - 客户端传来的
X-Expected-SHA256必须是 hex 编码小写字符串,对比前用strings.ToLower统一格式 - 校验失败时,除了返回 400,还要主动清理已生成的合并文件,防止残留脏数据
分片上传真正的复杂点不在切和合,而在状态一致性:上传 ID 生命周期管理、断点续传时的分片去重、分布式部署下的临时文件同步。这些没法靠单个 HTTP handler 解决,得配合 Redis 记录分片完成状态,或者用对象存储的 multipart API 替代自研。


















