核心在于服务端可靠接收、客户端精准定位、双方状态一致;须禁用默认 multipart 解析,避免 r.parsemultipartform 和 os.o_append 导致 90% 失败。

Go 实现大文件分片上传的断点续传,核心不在“怎么发请求”,而在于服务端能否可靠接收每一片、客户端能否精准定位未传位置、双方状态是否严格一致。硬套 r.ParseMultipartForm 或依赖 os.O_APPEND,90% 会失败。
服务端必须禁用默认 multipart 解析
Go 的 http.Request 默认在首次访问 r.MultipartForm 时自动调用 r.ParseMultipartForm(32 ,这会把整个请求体读进内存或临时文件,破坏流式写入能力,导致分片数据错位或丢失。
- 必须在 handler 开头立即调用
r.ParseMultipartForm(0)(传 0 表示禁用) - 元信息如
file_id、chunk_index应从 URL 查询参数或表单字段显式获取,别依赖r.MultipartForm.Value—— 它可能为空或尚未解析完成 - 分片二进制数据直接从
r.Body读取,用io.CopyN(dst, r.Body, chunkSize)写入磁盘,路径建议为./uploads/{file_id}/chunk_{index:04d} - 写入前用
os.MkdirAll确保目录存在;并发写同一分片时,用os.O_CREATE | os.O_WRONLY | os.O_EXCL打开文件,防止覆盖
客户端续传前必须验证服务端真实支持 Range
光看响应头有 Accept-Ranges: bytes 不够 —— Nginx、CDN 常伪造该 header,但实际对 Range 请求返回 200 或 500。
- 首次下载前,先发试探请求:
HEAD /file.zip或GET /file.zip带Range: bytes=0-1023 - 仅当响应码为
206 Partial Content且Content-Range匹配(如bytes 0-1023/12345678)才启用续传 - 若返回
200,说明服务端不支持,应清空本地文件重下;若返回416,需用HEAD获取真实长度再重试 - 每次请求的
Range头必须严格为bytes={offset}-,不能多空格、不能结尾带多余横杠
写文件必须用 Seek 而非 O_APPEND
os.O_APPEND 是断点续传最大陷阱:它会让 Write() 忽略之前所有 Seek(),强制写到末尾,导致数据错位或空白填充。
- 正确做法是用
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644)打开,再调file.Seek(offset, io.SeekStart) - Seek 后必须检查返回值,某些文件系统(如 NFS)可能不支持随机写
- 写入前建议调
file.Truncate(expectedSize),防止稀疏文件问题 - 别用
bufio.Writer包裹,缓冲会导致实际写入位置和 Seek 位置不一致
合并分片必须校验完整性且防 OOM
合并不是简单拼接,而是按序、无跳片、无重复,并最终校验整体哈希。否则上传成功却文件损坏,比失败更难排查。
- 收到最后一片时触发合并,但不能只信客户端传的
total_chunks—— 更可靠的是用 Redis 的 SET 存已传索引,或本地sync.Map计数 - 合并前逐片校验
chunk_hash(如 SHA256 前 8 字节),跳过等于放弃完整性保障 - 别用
io.Copy直接连串所有分片 —— 大文件易 OOM;改用os.Open每片 +io.CopyN分批写入目标文件 - 合并完成后,重新打开最终文件计算总哈希,与客户端提交的
X-Final-SHA256对比;不匹配则立刻删掉最终文件和所有分片
真正难的不是“怎么切片”或“怎么发请求”,而是服务端如何在无状态 HTTP 下持久化分片状态、客户端如何在弱网络中可靠探测服务端能力、以及合并那一刻如何避免因并发或校验缺失导致静默损坏。这些细节不处理好,上传成功率不会超过 70%。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











