续传必须用os.o_create|os.o_wronly打开文件并seek到指定offset,禁用os.o_append;每个分片需sha256校验,服务端提供get /upload/status接口返回已传chunk索引,客户端恢复前先head确认range支持。

Go 服务端怎么用 os.OpenFile 正确续传写入
必须禁用 os.O_APPEND,否则 file.Seek() 无效——这是 Go 运行时和底层文件系统的共同行为。续传本质是“从指定 offset 写”,不是追加。
- 打开方式应为:
os.O_CREATE | os.O_WRONLY,不加os.O_TRUNC,避免清空已有内容 - 先调
os.Stat()获取当前文件长度作为 offset,再file.Seek(offset, io.SeekStart) -
Seek()返回值必须与预期 offset 严格相等;不等说明文件被外部修改,应拒绝本次写入 - 写入后建议调
file.Sync(),尤其对小文件或关键业务,防止断电丢失最后几 KB
为什么不能靠 chunkIndex 拼接就完事
仅按序号拼接分片,等于放弃完整性保障。网络丢包、客户端重试、时钟漂移都可能导致重复、错序或伪造分片。
- 每个分片上传时,客户端必须计算并携带该分片的 SHA256(如
X-Chunk-Hash头),服务端收到后立即用crypto/sha256.New()+io.Copy()实时校验 - 校验失败返回
400 Bad Request并附带错误原因,客户端据此重传该 chunk - 合并前必须遍历所有已存分片文件,逐个校验 hash,缺一不可;跳过校验 = 接受静默损坏
- 不要用
io.Copy直接串连所有分片读取器——大文件易 OOM;改用os.Open单片 +io.CopyN分批写入目标文件
如何安全处理 HTTP 请求体避免内存爆掉
http.Request.Body 默认可能被 ParseMultipartForm 全部读进内存,对 GB 级文件就是灾难。必须绕过自动解析,流式接管原始 body。
- handler 开头调
r.ParseMultipartForm(0)(传 0 表示不限制内存缓存),强制标准库跳过自动消费 body - 用
r.URL.Query().Get("chunkIndex")和r.FormValue("totalChunks")提取元信息,别依赖r.MultipartForm.Value - 二进制数据直接从
r.Body读,配合io.LimitReader(r.Body, expectedSize)防止客户端谎报长度后发超额数据 - 写入磁盘路径建议按
./uploads/{uploadId}/chunk_{index}组织,避免并发覆盖;用os.O_CREATE | os.O_WRONLY | os.O_EXCL防止重复写入同一分片
客户端断点恢复时最常踩的坑
客户端以为“查到已传哪些 chunk 就能续”,但服务端状态若没及时持久化或没做一致性校验,续传就会写错位置甚至覆盖有效数据。
- 服务端必须提供轻量
GET /upload/status?file_id=xxx接口,返回已传 chunk 索引数组;推荐用 Redis SET 存储,SMEMBERS毫秒响应 - 客户端恢复前,应先发 HEAD 请求确认服务端是否支持 Range;若返回
200 OK而非206 Partial Content,说明服务端不支持,需全量重下 - Range 头格式必须严格为
bytes=1024-,不能多空格、不能写成bytes=1024-1024或bytes=1024-后跟多余字符 - 写入前建议先
file.Truncate(expectedTotalSize),防止 NFS 或某些文件系统产生稀疏文件,导致后续Seek()行为异常











