Go语言高性能并发下载器核心是控三件事:并发请求数(用semaphore.NewWeighted限流)、HTTP连接复用(全局复用*http.Client并配MaxIdleConns等参数)、文件写入不冲突(用os.File.WriteAt预分配后按偏移写)。
go 语言写高性能并发下载器,核心不是“开多少 goroutine”,而是控住三件事:并发请求数、http 连接复用、文件写入不冲突。盲目起 100 个 goroutine,大概率触发 context deadline exceeded、too many open files 或目标站返回 429 too many requests。
用 semaphore.NewWeighted 控制真实并发数
别用 time.Sleep 或裸 runtime.GOMAXPROCS 假限流——它们不拦住连接创建,只拖慢调度。真正要锁住的是「同时发起的 HTTP 请求」数量。
-
semaphore.NewWeighted(5)比带缓冲 channel 更安全:支持ctx取消,Acquire失败可立即退出,不会卡死 - 并发数别硬设 50:内网或 CDN 场景可试 20;公网批量下载建议 3–10;超过 20 很容易触发
net/http: request canceled (Client.Timeout) - 必须在每个 goroutine 里调用
defer sem.Release(1),且放在最外层defer链首——否则 panic 路径下会漏释放,后续所有任务永久阻塞
复用 *http.Client 并配死 Transport 参数
每个 goroutine 新建 http.Client 是最大性能杀手:DNS 缓存失效、TLS 握手重复、端口耗尽(Linux 下 TIME_WAIT 爆满)、连接池打穿。
- 全局复用一个
*http.Client,Timeout必设(如30 * time.Second),否则单个卡死请求会拖垮整个信号量池 -
Transport.MaxIdleConns设为 100~200(不是 0 或 10),MaxIdleConnsPerHost至少等于你下载 URL 涉及的域名数 × 5(比如下 3 个 CDN 域名,就设 15+) -
IdleConnTimeout必须设(如30 * time.Second),否则 NAT 网关或负载均衡器静默断连后,空闲连接永远滞留,最终dial tcp: lookup xxx: no such host
下载失败时必须关 resp.Body,哪怕 StatusCode != 200
这是最隐蔽的泄漏点:不关 resp.Body → 连接无法归还 idle 池 → 连接池慢慢枯竭 → 后续所有请求卡在 Get 阻塞 → 表现为“越跑越慢,最后全挂”。
- 正确写法是:
resp, err := client.Do(req)后立刻if resp != nil { defer resp.Body.Close() },不能只在err == nil分支里关 - 重试逻辑里更要小心:每次
Do都产生新resp,旧resp.Body必须已关闭,否则每轮重试都泄漏一个连接 - 用
io.Copy写文件,别用io.ReadAll—— 下载 500MB 文件,ReadAll就占 500MB 内存;io.Copy内存恒定几 KB
大文件分块下载必须用 os.File.WriteAt,禁用 O_APPEND
多个 goroutine 同时写一个文件,若用 os.O_APPEND + Write,结果必乱序/覆盖——因为 O_APPEND 强制每次写都在当前文件末尾,而多 goroutine 的“当前末尾”是竞态的。
- 打开文件用
os.O_CREATE | os.O_WRONLY,不用O_APPEND - 每个 goroutine 计算好自己的字节范围(如 chunk 0 写 offset 0,chunk 1 写 offset 1048576),调用
f.WriteAt(data, offset) - 务必预分配文件大小:
f.Truncate(totalSize),否则WriteAt到未分配区域会静默失败或填充零字节
真正难的不是写出来,而是把 resp.Body.Close() 塞进每一个错误分支、把 sem.Release(1) 放在 defer 最顶上、把 WriteAt 的 offset 算对——这些地方错一个,整批下载就悄无声息地崩掉。



















