go并发下载核心是控制并发数、复用http客户端并设超时、显式处理错误。推荐用semaphore.newweighted限制并发(如5),复用*http.client并配置超时与连接池,用errgroup.group收集错误,对500/503等状态码重试。

Go 语言做并发下载,核心不是“开多少 goroutine”,而是控制并发数、处理错误传播、避免资源耗尽。盲目用 go downloadFile(url) 启一堆协程,大概率触发 DNS 超时、连接池打满、目标站限流或本地文件句柄溢出。
如何安全地限制并发数(用 semaphore 或带缓冲 channel)
硬开 100 个 goroutine 下载 100 个文件,等于向系统提交 100 个 TCP 连接请求 + 文件写入操作,多数情况会失败。正确做法是用信号量或带缓冲 channel 控制同时活跃的下载任务数。
- 推荐用
golang.org/x/sync/semaphore:轻量、语义清晰,支持上下文取消 - 用带缓冲 channel(如
make(chan struct{}, 10))也能实现,但需手动放回 token,易漏、难调试 - 并发数别拍脑袋定:一般 3–10 较稳妥;若目标服务器响应快且带宽充足,可试 20;内网下载可更高
- 示例关键片段:
sem := semaphore.NewWeighted(5) // 最多 5 个并发<br>for _, u := range urls {<br> if err := sem.Acquire(ctx, 1); err != nil { return err }<br> go func(url string) {<br> defer sem.Release(1)<br> downloadFile(ctx, url)<br> }(u)<br>}
http.Client 必须复用并配置超时
每个 goroutine 新建一个 http.Client,会导致底层 http.Transport 实例爆炸,连接不复用、DNS 缓存失效、文件描述符快速耗尽。
- 全局复用一个
*http.Client,设置Timeout、Transport的MaxIdleConns和MaxIdleConnsPerHost - 不设超时的后果:某个 URL 卡住,整个 goroutine 永久阻塞,信号量无法释放,后续任务全堵死
- 典型配置:
client := &http.Client{<br> Timeout: 30 * time.Second,<br> Transport: &http.Transport{<br> MaxIdleConns: 100,<br> MaxIdleConnsPerHost: 100,<br> IdleConnTimeout: 30 * time.Second,<br> },<br>}
下载失败必须显式处理,不能只 log 后丢弃
并发场景下,单个失败不应中断整体流程,但也不能静默吞掉错误——否则你根本不知道哪几个文件没下下来。
- 用
sync.WaitGroup+chan error收集错误,或用errgroup.Group(更简洁) - 注意:不要在 goroutine 里直接
return或panic,会丢失调用栈且无法聚合结果 - 对 HTTP 状态码要区分:404 可记录后跳过;500/503 应重试(加简单退避);连接类错误(如
net/http: request canceled)大概率是上下文取消,不用重试 - 示例重试逻辑(最多 2 次):
for i := 0; i resp, err := client.Get(url)<br> if err == nil && resp.StatusCode return save(resp.Body)<br> }<br> if i == 2 { return err }<br> time.Sleep(time.Second }
真正难的不是并发本身,而是怎么让 10 个并发稳定跑完 1000 个链接——这取决于你对 http.Client 生命周期的理解、对错误分类的耐心、以及是否愿意为每个下载任务单独设 context.WithTimeout。漏掉任意一环,程序看起来“能跑”,实际在线上撑不过五分钟。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











