分片下载需服务端支持range头、客户端精准writeat、连接池调优三者缺一不可;必须用head请求验证accept-ranges: bytes并获取content-length,否则并发写将覆盖错乱。

分片下载不是开更多 goroutine 就能加速,核心是服务端支持 Range、客户端精准 WriteAt、连接池不卡死——三者缺一不可。
怎么确认目标服务器支持分片下载
发 HEAD 请求不是走形式,是必须前置验证的步骤。很多 CDN、API 网关、自研后端默认忽略 Range 头,你发了 GET 带 Range: bytes=0-1023,它照样返回 200 OK 和全量内容,协程间写文件直接覆盖错乱。
- 用
http.Head(url)获取响应,检查resp.Header.Get("Accept-Ranges") == "bytes" - 同时读取
resp.Header.Get("Content-Length")得到总大小,用于后续切分 - 别跳过这步直接上
GET+Range:curl -I -H "Range: bytes=0-1023" $url 能快速人工验证 - 若不支持,老老实实用单协程
io.Copy流式下载,或换源
并发写文件为什么不能用 os.O_APPEND
os.O_APPEND 是内核级追加语义,多个协程同时写,最终落盘位置由内核调度决定,完全不可控。你期望写入 [0, 1023] 和 [1024, 2047],实际可能变成两段都挤在开头或重叠。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 打开文件必须用
os.O_WRONLY | os.O_CREATE,**绝对不要加os.O_TRUNC**(否则一个协程清空,其他全白干) - 每个协程拿到
*os.File后,立刻调用f.Seek(offset, io.SeekStart)定位 - 写入必须用
f.WriteAt(data, int64(offset))或io.CopyN(f, resp.Body, size),确保字节数和偏移严格匹配 -
WriteAt是线程安全的(底层调用pwrite),无需额外加锁
为什么开了 10 个协程但实际只跑 2 个
默认 http.DefaultTransport 的 MaxIdleConnsPerHost 是 2,意味着每 host 最多缓存 2 个空闲连接。你启 10 个协程并发请求同一域名,其中 8 个会卡在连接池排队,等前面的请求释放连接——这不是并发,是串行排队。
- 显式配置
http.Transport:MaxIdleConnsPerHost至少设为协程数(如 20),IdleConnTimeout设为 30s 防堆积 - 复用同一个
*http.Client,别每个协程 new 一个——否则连接池、DNS 缓存全失效 - 给每个
http.Request加context.WithTimeout(ctx, 30*time.Second),防某次请求卡死拖垮全局 - 注意反爬:短时间大量
Range请求易被限速,可在协程启动前加time.Sleep(非写入后),比如time.Sleep(time.Millisecond * 100)
校验为什么不能只比对文件大小
Content-Length 是服务端声称的大小,os.Stat().Size() 是你写完后的磁盘大小——两者一致只说明“没丢片”,不保证“没写错”。网络中断、io.CopyN 返回字节数不足、磁盘满导致部分写失败,都会让某一段少几百字节,但总长仍对得上。
- 每个协程写完后,立即计算该段的
sha256.Sum256并存下来 - 所有协程结束后,再按顺序读取对应区间重新哈希,与预存值比对
- 或者全量计算最终文件的哈希,但大文件会阻塞主线程;分段校验可并行且定位精准
- 别依赖
resp.ContentLength做校验依据——它只是 header 字段,不是真实写入结果
最容易被忽略的是:分片区间必须严格连续、无重叠、无缺口,且最后一片的 end 必须是 total - 1(HTTP Range 是闭区间)。算错一个字节,整块就偏移,后面全乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










