支持分片下载需通过HEAD+Range试探请求验证,响应206且Content-Range匹配才可靠;多文件并发须复用http.Client、调优连接池参数、用semaphore限流;分片写入禁用os.O_APPEND,须显式Seek后Write,并始终defer resp.Body.Close。

不支持服务端 Range 的情况下,强行分片下载单文件会覆盖写入、校验失效;多文件并发下载则必须控制 goroutine 数量、复用 http.Client、显式关闭 resp.Body,否则很快触发连接池耗尽或文件描述符泄漏。
怎么判断目标 URL 是否支持分片下载
别只看 Accept-Ranges: bytes 响应头——Nginx 或 CDN 可能伪造它,但实际返回 200 OK 全量内容。真正可靠的方式是发一个试探性 HEAD 请求,再补一个带 Range: bytes=0-1023 的 GET:
- 响应码为
206 Partial Content且Content-Range头匹配(如bytes 0-1023/12345678)→ 支持分片 - 响应码为
200 OK→ 不支持,改用单协程流式下载 - 响应码为
416 Requested Range Not Satisfiable→ 文件长度可能已变,需重新HEAD获取最新Content-Length
Go 中建议封装成独立函数,返回 size int64 和 supportRange bool,后续逻辑据此分支处理。
多文件并发下载如何避免连接池打满
默认 http.DefaultTransport 只允许每 host 最多 2 个空闲连接,开 10 个 goroutine 下载不同 URL,8 个会卡在连接池排队,实际并发远低于预期,还容易超时。
立即学习“go语言免费学习笔记(深入)”;
- 全局复用一个
*http.Client,不要每个 goroutine 新建一个 -
Transport.MaxIdleConns和MaxIdleConnsPerHost都设为 ≥ 并发数(如 20),防止新建连接风暴 -
IdleConnTimeout设为 30s 左右,防连接堆积 - 给每个请求加
context.WithTimeout,避免单个卡死拖垮整组任务
错误示范:go func() { http.Get(url) }();正确做法:用 semaphore.NewWeighted(5) 控制并发 + 复用 client + 显式 timeout。
分片写入文件为什么不能用 os.O_APPEND
os.O_APPEND 是并发写同一文件的最大陷阱:它会让每次 Write() 强制跳到当前文件末尾,完全忽略之前 Seek() 的偏移。结果就是多个协程写的段全堆在末尾,中间大片空白或错位。
- 打开文件必须用
os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0644),禁用os.O_TRUNC和os.O_APPEND - 每个协程拿到
*os.File后,立刻调用f.Seek(offset, io.SeekStart),并检查返回值是否等于offset - 写入用
io.CopyN(f, resp.Body, size),确保字节数精准,不依赖Content-Length - 所有协程写完后,主协程统一
Close(),协程间不共享resp.Body
即使只下 1 个大文件,只要用了多协程分片,就必须走这套 Seek+Write 流程,O_APPEND 在这里没有安全场景。
下载失败时 resp.Body.Close() 容易被忽略
这是 Go 并发下载最隐蔽的资源泄漏点:只要 resp != nil,无论 StatusCode 是多少、err 是否非空,都必须关 resp.Body。漏掉就会导致底层 TCP 连接无法释放,连接池迅速枯竭。
- 正确姿势是
defer resp.Body.Close(),且必须放在if err != nil判断之后、任何分支之前 - 如果中间有重定向或鉴权逻辑,确保每个
resp分支都有defer或显式Close() - 别在
if resp.StatusCode != 200后直接 return —— 此时resp.Body还开着
并发场景下,一次漏关可能影响后续几十个请求;而这个问题在线上往往表现为“越跑越慢”,排查成本远高于写的时候多加一行 defer。


















