直接用http.get无法支持多并发下载,因其默认单连接且response.body为单次流,不支持分段读取;必须通过http range请求配合多个goroutine并发拉取不同字节段,并先head确认服务端支持accept-ranges: bytes及content-length有效性。

为什么直接用 http.Get 无法支持多并发下载
因为 http.Get 默认每次只建立一个连接,且返回的 *http.Response.Body 是单次读取流,无法分段、随机 seek 或并行读取。想并发下载同一文件,必须靠 HTTP Range 请求配合多个 client 并发拉取不同字节段。
常见错误是没设 Content-Length 头或服务端不支持 Accept-Ranges: bytes,导致 range 请求被忽略,返回整个文件或 200 而非 206 —— 这会让多个 goroutine 重复下载相同内容,甚至写入冲突。
- 先用 HEAD 请求确认服务端是否支持 range:
resp.Header.Get("Accept-Ranges") == "bytes" - 检查
Content-Length是否存在且为正数,否则无法分块 - 每个 goroutine 必须独立创建
http.Client(或复用但禁用 redirect/cookie 等副作用)
如何安全地分块并写入同一个文件
不能让多个 goroutine 直接 os.Write 到同一 *os.File,会因 offset 不同步导致数据错乱。正确做法是:用 file.Seek() 定位后调用 file.Write(),且每段写入必须原子完成。
关键点在于:每个 goroutine 拿到自己的起始偏移和长度,打开文件时加 os.O_WRONLY 但不加 os.O_TRUNC,然后 Seek(offset, 0) 再 Write()。即使并发写,只要 offset 不重叠,就不会冲突。
- 分块大小建议设为 1–5MB,太小增加 HTTP 开销,太大降低并发收益
- 写入前务必检查
err := file.Seek(offset, 0)是否成功,失败要 panic 或重试 - 不要用
bufio.Writer包裹文件,它会缓存并破坏精确 offset 控制
io.CopyN 和手动 io.ReadFull 哪个更适合分块下载
io.CopyN 更简洁,但内部不保证“恰好读满 N 字节”——如果响应流提前 EOF,它会返回 io.ErrUnexpectedEOF,而你其实期望的是 206 响应中 body 长度严格等于请求的 range 长度。一旦出错,很难区分是网络中断还是服务端 bug。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
更稳妥的做法是用 io.ReadFull(dst, buf) 配合预分配缓冲区,确保读满指定字节数,否则明确失败。
- 用
buf := make([]byte, chunkSize)分配固定大小缓冲区 - 调用
n, err := io.ReadFull(resp.Body, buf),若err == io.EOF || err == io.ErrUnexpectedEOF,说明服务端异常截断 - 写入时直接
file.Write(buf),无需再判断n——ReadFull已保证n == len(buf)
如何处理部分失败与断点续传
并发下载中某个 chunk 失败(如超时、连接关闭),不应全盘重来。应记录已成功写入的 byte range,下次启动时跳过这些区间。
最轻量做法:下载前先 os.Stat 目标文件,获取当前长度,作为已下载偏移;再结合服务器返回的 Content-Length 计算剩余需下载范围。但这只适用于从头开始或完全中断的情况。
- 真断点续传需要额外存储已下载的 range 列表(如 JSON 文件),每次启动读取并跳过
- HTTP client 要设置合理超时:
http.Client{Timeout: 30 * time.Second},避免卡死 goroutine - 失败 chunk 可重试 2–3 次,但需指数退避,避免雪崩请求
并发下载的核心不是“快”,而是“可控”——range 边界、文件定位、错误隔离,这三处任一出错都会导致文件损坏,比单线程慢更糟。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










