应复用 http.Client 并配置 Transport 参数限流,而非盲目启大量 goroutine;设 MaxIdleConns 和 MaxIdleConnsPerHost 为 100、IdleConnTimeout 为 30s,并用带缓冲 channel(如 make(chan struct{}, 10))控制并发数。

用 http.Client 控制并发,别直接开几百个 go 语句
开一堆 go downloadFile(url) 看似简单,实际会瞬间打爆本地文件描述符、触发 DNS 超时、被目标站限流甚至封 IP。根本问题不在协程本身,而在没管住底层 HTTP 连接和请求节奏。
正确做法是复用 http.Client,并配好 Transport:
-
MaxIdleConns和MaxIdleConnsPerHost设为足够大(比如 100),避免频繁建连 -
IdleConnTimeout设短一点(如 30s),防连接堆积 - 给每个
http.Request加上Context,支持超时和取消
示例关键片段:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}
用 semaphore 或带缓冲 channel 限流,不是靠 time.Sleep
time.Sleep 是假限流:它不阻止 goroutine 创建,只延迟执行,内存和调度开销照旧。真正要控的是「同时发起的请求数」。
推荐用带缓冲的 channel 模拟信号量:
- 初始化
sem := make(chan struct{}, 10)表示最多 10 个并发 - 每个下载前写入:
sem - 下载完记得
归还额度
注意别漏掉归还——尤其在错误路径里。漏了就卡死。
下载失败时别忽略 resp.Body.Close(),否则连接泄漏
这是 Go 并发下载最隐蔽的泄漏点。哪怕 resp.StatusCode != 200 或 err != nil,只要 resp 非 nil,resp.Body 就必须关。
常见错误写法:
resp, err := client.Get(url)
if err != nil { return }
if resp.StatusCode != 200 { return } // ← 这里 resp.Body 没关!
正确姿势是用 defer,但得小心作用域:
resp, err := client.Get(url)
if err != nil { return }
defer resp.Body.Close() // 放这里才对
如果中间有重定向或鉴权逻辑,也要确保每个分支都覆盖到 Close()。
写文件时用 io.Copy,别自己 Read/Write 循环
手动循环读写不仅啰嗦,还容易出错(比如没处理 io.EOF、缓冲区大小不合理)。而且小缓冲区会放大系统调用次数,大缓冲区又浪费内存。
io.Copy 内部用了 32KB 缓冲,平衡了性能和内存,且自动处理各种边界条件。
- 别:
for { n, _ := src.Read(buf); dst.Write(buf[:n]) } - 要:
n, err := io.Copy(dst, src),然后检查err和n
如果需要进度回调,再包一层 io.Reader 实现 Read 方法即可,不用动主逻辑。
并发下载真正的复杂点从来不在“怎么开 goroutine”,而在于连接复用、资源释放、错误传播这三件事是否闭环。少一个,跑一小时就崩一次。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











