Go大文件分片下载需手动管理HTTP Range头,核心是构造带Range头的GET请求并校验206响应;须先HEAD检查Accept-Ranges,用WriteAt并发写、semaphore限流、.meta持久化状态、流式读写防OOM,并处理416等边界错误。

Go大文件分片下载必须手动管理HTTP Range头
Go标准库的http.Client本身不支持自动分片,分片逻辑完全由你控制。核心是为每个分片构造带Range请求头的GET请求,并确保服务端返回206 Partial Content而非200 OK。如果服务端不支持断点续传(比如Nginx未启用accept_ranges: bytes),所有分片请求都会退化成全量响应,反而更慢。
实操建议:
- 先发一个
HEAD请求,检查响应头是否含Accept-Ranges: bytes,否则直接放弃分片,走单连接下载 - 用
req.Header.Set("Range", "bytes=0-1048575")指定字节范围,注意末尾偏移量要≤文件总大小−1 - 每个分片需独立
http.Client或复用但显式关闭响应体:resp.Body.Close(),否则连接不会复用,还可能触发too many open files - 不要用
io.Copy直接写入同一*os.File——多个goroutine并发写会覆盖,改用file.WriteAt(data, offset)
并发分片数不是越多越好,通常设为3–5最稳
盲目提高goroutine数量(比如开50个分片)反而降低吞吐:TCP连接竞争、系统文件描述符耗尽、磁盘随机写放大、服务端限流触发。实测在千兆内网+SSD环境下,分片数超过5后总耗时基本持平甚至上升。
推荐做法:
- 用
semaphore.NewWeighted(4)(需引入golang.org/x/sync/semaphore)限制并发请求数 - 每个分片任务封装为函数,接收
start, end int64和*os.File,内部负责重试(最多2次)、超时(建议context.WithTimeout(ctx, 30*time.Second)) - 记录每个分片的
Content-Range响应头,校验实际返回字节数是否匹配预期,防止服务端静默截断
恢复断点续传的关键是本地分片状态持久化
程序崩溃或网络中断后,若不记录哪些分片已完成,重启就得全部重下。不能只依赖文件大小判断——因为WriteAt可能写入部分数据但没刷盘,文件长度虽变但内容不完整。
轻量方案:
- 下载前生成同名
.meta文件(如archive.zip.meta),用JSON存每个分片的{start, end, done: true/false} - 每次成功写完一个分片,立刻
f.Sync()并更新.meta中对应项的done字段 - 启动时先读
.meta,跳过done: true的区间,仅发起剩余分片请求 - 下载完成后删掉
.meta,避免残留垃圾文件
避免 ioutil.ReadAll 导致OOM,流式处理响应体
分片大小设为10MB时,若用ioutil.ReadAll(resp.Body),内存会瞬间吃掉10MB × 并发数。尤其在嵌入式设备或容器内存受限场景,极易触发OOM kill。
正确方式是边读边写:
buf := make([]byte, 32*1024)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, writeErr := file.WriteAt(buf[:n], offset)
if writeErr != nil {
return writeErr
}
offset += int64(n)
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
缓冲区用32KB是平衡CPU和IO的常见值;offset需从该分片起始位置开始累加,不能用文件当前长度——因为其他分片可能还没写完。
分片下载真正难的不是并发,而是状态一致性和错误边界处理。比如服务端突然返回416 Range Not Satisfiable,说明文件被修改过,此时应整体重新获取Content-Length并重置所有分片计划——这点90%的开源实现都漏了。











