断点续传需服务端持久化维护上传状态,包括file_id、uploaded_bytes等字段,使用SQLite/Redis等介质存储;须解析Content-Range头校验偏移,原子更新状态与文件写入,合并时校验并安全清理临时文件。

断点续传必须自己维护上传状态,Go 标准库不提供
Go 的 http.Client 和 multipart.Writer 本身不记录上传进度或断点位置。你得在服务端显式保存每个文件的已接收字节偏移、分片哈希、上传是否完成等状态。否则客户端重试时,服务端只能从头收——这不是断点续传,是“重复上传”。
常见错误是把状态存在内存(比如用 map[string]int64),重启后全丢;或者用文件但没加锁,多协程写同一状态文件导致偏移错乱。
- 状态至少包含:
file_id、uploaded_bytes、total_size、last_modified、status("uploading"/"completed"/"failed") - 推荐存到轻量级持久化介质:SQLite(单机)、Redis(支持过期+原子操作)、或带事务的 PostgreSQL 表
- 每次接收分片前,先查状态确认
uploaded_bytes是否匹配客户端传来的offset,不匹配就返回409 Conflict
HTTP 分片上传接口设计要兼容 Range 和 Content-Range
客户端通常用 Content-Range 头标识当前分片位置,例如 bytes 1024-2047/10485760。服务端不能只靠 URL 参数或 JSON body 解析偏移——那是不可靠的,也违反 HTTP 协议惯例。
Go 的 http.Request.Header.Get("Content-Range") 返回原始字符串,需手动解析。别用正则硬拆,用标准库 net/http/httpguts.ParseContentRange(Go 1.22+)或自己写简单 parser。
立即学习“go语言免费学习笔记(深入)”;
- 解析失败或范围越界(如
1024-2047超过total_size)直接返回416 Range Not Satisfiable - 写入文件时,用
os.OpenFile(..., os.O_WRONLY|os.O_CREATE)+file.Seek(offset, io.SeekStart)+io.CopyN确保只写对应区间 - 避免用
os.WriteFile全量覆盖,它会清空整个文件
并发上传多个分片时,状态更新和文件写入必须原子
用户可能开多个 tab 或多线程同时上传不同分片,如果状态更新(DB 写)和文件写入(Seek+Write)不在同一事务里,会出现“状态说写了 5MB,但文件只有 3MB”的不一致。
最简方案:对同一 file_id 加分布式锁(Redis SET key value EX 30 NX);或本地用 sync.Map + sync.Mutex 配合文件名做 key(仅限单进程)。但注意:锁粒度不能是整个上传服务,否则变成串行上传。
- 推荐组合:Redis 锁(控制并发写) + SQLite WAL 模式(保证状态写入原子) + 文件系统追加写(用
O_APPEND替代Seek,但需客户端按顺序传分片) - 更稳妥的是用单个
*os.File句柄 +sync.RWMutex控制读写,避免频繁 Open/Close 导致句柄耗尽 - 别在 HTTP handler 里做耗时状态同步(如远程调用鉴权服务),应异步落库或预加载
客户端校验失败时,服务端要能安全回滚部分写入
有些客户端会在最后发一个 MERGE 请求合并分片,并附上所有分片的 SHA256。如果校验失败,你不能只删状态记录——对应分片数据还躺在磁盘上,下次同名上传会复用旧偏移,导致文件损坏。
所以状态表里得有 temp_file_path 字段,指向实际存储路径(如 /tmp/upload_abc123.bin)。校验失败时,删状态行 + os.Remove(temp_file_path) 必须成对出现,建议封装为事务函数。
- 临时文件路径别用
os.TempDir(),它可能被系统清理;固定目录 + 定期扫描last_modified < time.Now().Add(-24h)的残留文件 - 校验逻辑放 merge 阶段,不要每片都算 hash(性能差),但要在 merge 前检查所有分片是否收齐(
uploaded_bytes == total_size) - 返回给客户端的错误信息要具体,比如
{"error": "checksum_mismatch", "expected": "a1b2...", "got": "c3d4..."},方便前端 debug
真正麻烦的不是代码怎么写,而是状态生命周期管理:上传中、中断后、超时后、校验失败后、客户端重复发起 merge 后……每个环节都要定义清楚状态迁移规则和清理边界。


















