应复用单个http.Client并调大MaxIdleConns和MaxIdleConnsPerHost;多goroutine写文件须用WriteAt配合预分配和互斥offset;断点续传需HEAD校验Range支持并分块SHA256校验;并发控制用带缓冲channel,失败按错误类型指数重试。

并发下载时如何避免 http.Client 被复用导致连接泄漏
Go 的 http.Client 默认复用底层 TCP 连接,但若在并发场景中反复新建 http.Client 实例(比如每个 goroutine 都 new 一个),会快速耗尽文件描述符;反之,若全局只用一个 http.Client 却不设限,又可能因默认的 MaxIdleConns 和 MaxIdleConnsPerHost 过小,导致大量请求排队等待空闲连接。
正确做法是复用单个 http.Client,并显式调大连接池容量:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
},
}- 不要在 goroutine 内部创建新
http.Client -
MaxIdleConnsPerHost必须设为 ≥ 并发数,否则实际并发量会被 transport 拦截限制 - 若目标服务器域名分散(如 cdn1.example.com、cdn2.example.com),
MaxIdleConnsPerHost才真正起作用;同一 host 下仍受MaxIdleConns总量约束
如何安全地用多个 goroutine 写入同一个文件
直接让多个 goroutine 同时 os.Write 到一个 *os.File 会导致数据错乱或 panic —— 文件偏移量(offset)不是 goroutine 安全的。常见错误是:每个 goroutine 自行计算分块起始位置后调用 file.Write(),结果写入位置互相覆盖。
必须使用 file.WriteAt(),它按指定 offset 写入,且内部加锁保证原子性:
立即学习“go语言免费学习笔记(深入)”;
_, err := file.WriteAt(chunkData, int64(startOffset))
if err != nil {
// 处理写入失败,如磁盘满、权限不足
}- 确保每个 goroutine 分配到互不重叠的
startOffset和chunkData长度 - 提前用
file.Truncate(totalSize)预分配文件大小,避免稀疏文件和扩容竞争 - 不要用
file.Seek() + file.Write()组合,它在并发下不可靠
如何实现断点续传与分块校验
HTTP 下载中断后从头开始太浪费。关键在于:先 HEAD 请求获取 Content-Length,再根据本地已存在文件大小决定是否续传;同时每块下载完应校验 SHA256,防止网络传输损坏。
一套面向小企业用户的企业网站程序!功能简单,操作简单。实现了小企业网站的很多实用的功能,如文章新闻模块、图片展示、产品列表以及小型的下载功能,还同时增加了邮件订阅等相应模块。公告,友情链接等这些通用功能本程序也同样都集成了!同时本程序引入了模块功能,只要在系统默认模板上创建模块,可以在任何一个语言环境(或任意风格)的适当位置进行使用!
典型流程:
- 发起
HEAD请求,检查响应头Content-Length和Accept-Ranges: bytes - 读取本地文件长度
stat.Size(),若 > 0 且Content-Length > stat.Size(),则用Range: bytes=xxx-续传 - 每块数据写入前计算
sha256.Sum256(chunkData),与服务端提供的ETag或预置 checksum 对比(若支持)
注意:ETag 不一定代表完整文件哈希,有些 CDN 返回的是 inode 或 MD5,不能直接用于分块验证;稳妥做法是服务端额外提供 X-Checksum-Sha256 响应头。
如何控制并发数并处理失败重试
无限制启动 goroutine 容易打爆内存或触发目标服务器限流。应使用带缓冲的 channel 控制并发数,并对失败请求做指数退避重试。
sem := make(chan struct{}, 10) // 最多 10 个并发
for i, task := range tasks {
go func(t DownloadTask, idx int) {
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放信号量
downloadWithRetry(t, idx)
}(task, i)
}- 别用
sync.WaitGroup单纯等 goroutine 结束,它不控并发;semchannel 是轻量且精确的节流方式 - 重试逻辑里要区分错误类型:DNS 失败可立即重试,404 或 416(Range Not Satisfiable)应直接失败,5xx 错误适合指数退避(如 1s、2s、4s)
- 每次重试前检查本地文件是否已被其他 goroutine 写满,避免重复下载同一块
分块下载看似简单,但 offset 对齐、连接复用、写入原子性、错误恢复这四点任意一个没处理好,都会导致文件损坏或性能骤降。尤其是 WriteAt 的 offset 计算和 http.Transport 参数调优,最容易被忽略。









