go语言分片上传需自行实现状态管理;正确做法是用io.readat按绝对偏移读取,先stat获取文件大小计算总片数,再以partindex*chunksize为偏移调readat,严格检查返回长度n。

Go 语言没有分片上传的“开箱即用”支持,http.Client 不管你传到第几块、哪块失败了、要不要重试——所有状态管理、偏移控制、并发协调、断点续传逻辑都得你自己写。
怎么用 io.ReadAt 安全读取指定分片
错误做法是用 os.ReadFile 或循环调 file.Read 累积偏移,容易错位或内存爆掉。正确方式是打开文件后,用线程安全的 io.ReadAt 按绝对偏移读固定长度:
- 先
file.Stat()拿到大小,算总片数:totalParts := int((size + chunkSize - 1) / chunkSize) - 每片调
n, err := file.ReadAt(buf[:], int64(partIndex)*int64(chunkSize)),不依赖上一次读完的位置 - 必须检查
n ,最后一片要显式截断:<code>data := buf[:n],否则可能带前一片残留数据 - 别用
bufio.Reader包裹文件再调ReadAt——它内部维护缓冲区,会破坏随机读语义
为什么不能用 base64 + JSON 传分片
常见错误是把二进制分片 base64.StdEncoding.EncodeToString(chunkData) 塞进 JSON 字段上传。这会导致:
- 内存占用暴涨约 1.3 倍(
base64编码膨胀 + JSON 字符串堆分配),GC 压力陡增,高并发时易触发 STW - 服务端直接拒收:对象存储(MinIO/OSS/TUS)只认
multipart/form-data或application/octet-stream,收到 JSON 就返回400 Bad Request并带"invalid part data" - 若坚持用 raw body,必须设
req.Header.Set("Content-Type", "application/octet-stream"),且req.Body = io.NopCloser(bytes.NewReader(data))
并发上传必须限流,且等全部成功才合并
开 20 个 goroutine 同时上传 20 个分片?网络抖动时大量超时、TCP 连接耗尽、重试雪崩,反而比串行慢。更危险的是部分成功就调合并接口,服务端返回 404 Missing Part:
- 用
semaphore或带缓冲 channel 控制并发数(建议 3–5) - 每个分片上传后必须校验服务端返回的
ETag(如 MinIO 的 MD5)是否匹配本地计算值 - 用
sync.WaitGroup等所有 goroutine 返回,任一失败就中断,不触发合并 - 失败分片需记录
uploadID+partNumber到本地磁盘(如 SQLite 或 JSON 文件),下次启动先查再跳过已成功项
断点续传靠服务端状态,不靠客户端缓存
客户端本地存“上次传到第几块”不可靠——浏览器清缓存、设备切换、进程被杀都会丢数据。真正可用的方案是:
- 客户端上传前先发 GET 请求到
/upload/status?upload_id=xxx,拿到服务端已确认接收的分片列表 - 服务端必须维护一份轻量状态,比如内存 map 或 Redis 中的
{upload_id → map[int]bool} - 服务端生成 upload_id(如 UUID),禁止客户端自己拼,避免并发冲突覆盖
- 重传某分片时,服务端应幂等处理:检查该分片是否已存在且大小/哈希一致,一致则直接返回成功,不重复写磁盘
最常被忽略的一点:服务端合并时绝不能把所有分片读进内存再写,而要用 io.Copy 流式按序写入目标文件——否则 10GB 文件会吃光几十 GB 内存;另外,os.Stat().Size 不可靠,文件正被其他进程写入时该值可能不准,应改用 file.Seek(0, io.SeekEnd) 获取实时大小。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











