断点续传必须服务端持久化存储上传状态,因go标准库不记录进度;禁用内存map或无锁临时文件,推荐sqlite/redis/postgresql存储file_id、uploaded_bytes、total_size、status等字段,并在每次分片上传前校验content-range偏移一致性。

断点续传必须自己存状态,Go 标准库不提供上传进度记忆能力——r.ParseMultipartForm 不记 offset,http.Client 不存分片位置,重启后全丢是常态。
为什么不能用内存 map 或临时文件存状态
常见错误是把 uploaded_bytes 存在 sync.Map 或本地 JSON 文件里:
- 进程重启或滚动更新后,
map里所有状态清零,客户端重试只能从头传 - 多个实例(比如 Kubernetes 多副本)时,各节点 map 完全隔离,状态无法共享
- 并发写同一状态文件没加锁,
uploaded_bytes被覆盖或错乱,出现“已传 5MB”但文件只有 2MB 的不一致
推荐的持久化介质选型与关键字段
状态必须落盘且跨进程/跨实例可见。选型取决于部署规模:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 单机部署:用
SQLite,开启 WAL 模式,支持事务原子写入file_id、uploaded_bytes、total_size、status("uploading"/"completed"/"failed")、last_modified - 多实例或需自动过期:用
Redis,SET upload:abc123 "{...}" EX 86400,配合INCRBY原子更新偏移量 - 已有 PostgreSQL:建一张
upload_status表,主键为file_id,所有更新走UPDATE ... WHERE file_id = ? AND uploaded_bytes = ?防覆盖
状态读写时机与校验逻辑
每次接收分片前,必须查状态并严格比对,不是可选项:
- 用
r.Header.Get("Content-Range")解析出客户端声明的 offset(如bytes 1024-2047/10485760),调用net/http/httpguts.ParseContentRange(Go 1.22+)或手写 parser - 查 DB 得到当前
uploaded_bytes,若不等于请求 offset,直接返回409 Conflict - 写入新分片后,必须在同一事务/原子操作中更新
uploaded_bytes和last_modified,避免状态和文件不同步 - 合并完成时,把
status改为"completed",并设 TTL 清理(如 RedisEXPIRE或 SQLiteDELETE WHERE status = 'completed' AND last_modified )
容易被忽略的边界点
状态持久化最脆弱的地方不在主流程,而在异常路径:
- 客户端中断上传后,没人主动清理状态——得靠定时任务扫描
status = "uploading"且last_modified 的记录 - 分片写入失败(磁盘满、权限不足)时,如果只 rollback DB 状态但没删掉已写入的临时分片,下次合并会出错
- 用
Redis时,SET key value EX 30 NX加锁必须配超时,否则死锁卡住整个 upload 流程
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










