上传进度不能只存在内存里,因为进程重启、服务崩溃或网络中断后,map[string]int或临时变量里的进度就丢了,用户需从0%重来,体验差且浪费带宽;真正可用的进度存储必须落盘或进数据库,并通过upload_id或文件哈希唯一关联。

上传进度为什么不能只存在内存里
因为进程重启、服务崩溃或网络中断后,map[string]int 或临时变量里的进度就丢了。用户重新上传同个文件时,得从 0% 重来,体验差,还浪费带宽。真正可用的进度存储必须落盘或进数据库,且要能和上传请求唯一关联——通常靠 upload_id 或文件哈希。
用 SQLite 存进度比用 Redis 更稳妥
小团队或单机部署时,SQLite 足够轻量,不用额外维护服务,ACID 也能保证写入不丢。Redis 虽快,但默认 RDB 持久化有间隔,AOF 又影响性能;万一 Redis 挂了,刚写入的进度可能没刷盘。SQLite 文件放在 ./data/upload_progress.db,建表语句要包含必要字段:
CREATE TABLE IF NOT EXISTS upload_progress (
upload_id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
total_size INTEGER NOT NULL,
uploaded_bytes INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status TEXT CHECK(status IN ('uploading', 'completed', 'failed')) DEFAULT 'uploading'
);
-
upload_id必须由客户端传入或服务端生成(推荐用uuid.NewString()),不能依赖 request ID,因为分片上传会有多次请求 - 每次收到分片,执行
INSERT OR REPLACE INTO upload_progress ...,避免事务锁表太久 - 更新
uploaded_bytes时,用uploaded_bytes = uploaded_bytes + ?而非直接赋值,防止并发覆盖
HTTP 分片上传中如何安全更新进度
Go 的 http.Request 默认不支持断点续传的 Range 解析,得手动读取 multipart 表单里的分片序号、总片数、当前偏移量。关键不是“怎么读”,而是“什么时候写”:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 务必在
os.Write成功写入磁盘后,再调用数据库UpdateProgress()函数,否则出现“文件已存但进度没更新”的不一致 - 不要在每个分片都触发一次
UPDATE,可加简单缓冲:每 5 个分片或间隔 2 秒才刷一次库,用time.AfterFunc或 channel 控制 - 如果用
io.Copy直接流式写入,无法获知已写多少字节,必须用io.MultiWriter包裹一个计数器counter := &ByteCounter{},再传给io.Copy
客户端查询进度时的边界情况处理
前端轮询 /api/progress?upload_id=xxx,后端查库返回 JSON,但容易忽略这些细节:
立即学习“go语言免费学习笔记(深入)”;
- 查不到
upload_id时,别返回 404,统一返回{"status": "unknown", "progress": 0},避免前端反复重试或报错 -
status = 'completed'时,uploaded_bytes应等于total_size,但实际可能因校验失败被标记为 completed 却少写了字节,所以返回前要再校验一次文件长度 - 超过 24 小时未更新的记录,建议加
WHERE updated_at > datetime('now', '-24 hours')过滤,避免垃圾数据堆积
最麻烦的不是写代码,是设计 upload_id 的生命周期管理——它该不该自动清理?清理时机是成功后立刻删,还是留 7 天供重试?这个决策直接影响数据库膨胀速度和用户体验一致性。

















