断点续传必须用 os.O_CREATE | os.O_WRONLY 而非 os.O_APPEND,因后者强制写入末尾且忽略 Seek;需先 Stat 获取长度作为 offset,再 Seek 验证,配合 Range 头、206 响应、io.Copy 与 sha256.New() 同步流式校验,分片须逐片哈希比对,最终文件需原子重命名并全量校验。
断点续传必须用 os.O_CREATE | os.O_WRONLY,不能用 os.O_APPEND
因为 os.o_append 会强制将每次写入位置跳转到文件末尾,哪怕你调用 file.seek() 也无效——这是 posix 和 go 运行时的共同行为。断点续传的核心是“从指定偏移位置继续写”,不是追加。
实操要点:
- 先用
os.Stat()获取已存在文件长度,作为起始offset - 用
os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)打开(不加os.O_TRUNC) - 立即调用
file.Seek(offset, io.SeekStart),并检查返回值是否等于offset;不等说明文件被篡改或并发写入冲突 - HTTP 请求头需带
Range: bytes=1024-,服务端响应状态码应为206 Partial Content
io.Copy 流式写入 + sha256.New() 同步校验
不要等整个文件下载完再算 SHA256,而是在写入过程中同步更新哈希器。Go 的 hash.Hash 接口实现了 io.Writer,可直接作为 io.Copy 的目标。
常见错误现象:手动分块读取再 hasher.Write(),既冗余又易出错;或把整个响应体读进 []byte 再计算,导致 GB 文件 OOM。
正确做法:
- 创建
hasher := sha256.New() - 用
io.Copy(io.MultiWriter(file, hasher), resp.Body)一次性完成写入与哈希更新 - 若需校验每一块(如分片上传),可用
io.TeeReader(chunkReader, hasher)边读边喂给哈希器 - 最终调用
hex.EncodeToString(hasher.Sum(nil))得到十六进制字符串
分片上传中校验失败时,别静默跳过 chunk_hash
只靠 chunk_index 拼接文件不可靠——网络丢包、重复发送、客户端时钟漂移都可能导致错序或重复。内容校验不是可选项,是保底机制。
服务端接收每个分片时必须:
- 客户端随请求发送
X-Chunk-Hash: a1b2c3...(完整 SHA256 或截取前 8 字节) - 服务端用
io.Copy(hasher, chunkReader)实时计算,不落地再读 - 比对失败立即返回
400 Bad Request或416 Range Not Satisfiable,附带正确 offset - 合并前再次按
chunk_index排序,并逐片验证本地计算的 hash 与客户端声明一致
临时文件 + 原子重命名才是安全落地的关键
直接写目标文件风险极高:中断时留下损坏文件、并发写入覆盖、权限丢失。生产环境必须用两阶段提交。
推荐路径结构:./uploads/{file_id}/part_{chunk_index}(每个分片独立临时文件) → 合并到 ./uploads/{file_id}/.tmp_final → 最后 os.Rename() 替换目标文件。
注意点:
- 合并时不建议
cat所有分片或io.Copy串连,容易 OOM;应循环os.Open每个分片 +io.CopyN(dst, src, size)分批写入 - 合并前必须检查
len(chunkFiles) == total_chunks,缺一不可 - 校验最终文件的完整 SHA256,和客户端最初声明的
file_hash对齐,才算真正闭环











