HTTP默认客户端并发性能差因Transport连接限制:MaxIdleConns=100、MaxIdleConnsPerHost=2,易触发连接阻塞和“too many open files”错误;需自定义Transport并合理配置超时、限流与context控制。

为什么 http.DefaultClient 默认不支持高并发?
Go 的 http.DefaultClient 本身是线程安全的,能并发调用 Do(),但它的底层 Transport 默认只允许最多 100 个空闲连接(MaxIdleConns),且对同一 host 限制 2 个空闲连接(MaxIdleConnsPerHost)。这意味着:即使你开 1000 个 goroutine,并发请求数很快会被阻塞在连接复用队列里,实际吞吐卡在个位数。
常见现象:请求耗时陡增、大量 net/http: request canceled (Client.Timeout exceeded while awaiting headers) 或 dial tcp: too many open files 错误。
- 必须显式配置
http.Transport,尤其调大MaxIdleConns和MaxIdleConnsPerHost -
IdleConnTimeout和TLSHandshakeTimeout建议设为 30s 左右,避免连接僵死 - 若目标服务支持 HTTP/2,确保 Go 版本 ≥ 1.6 且服务端开启,它能复用单连接多路请求,比 HTTP/1.1 更省资源
如何用 sync.WaitGroup + goroutine 安全控制并发请求?
直接起一堆 goroutine 调 http.Client.Do() 很容易失控:没限速、没错误收集、没超时统一管理。用 sync.WaitGroup 是最轻量的协调方式,但要注意别漏掉 defer wg.Done() 或提前 return 导致计数不匹配。
更关键的是:别把 http.Request 对象在多个 goroutine 间共享 —— 它不是线程安全的,每次请求必须新建。
立即学习“go语言免费学习笔记(深入)”;
func fetchURLs(urls []string) []error {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
},
}
var wg sync.WaitGroup
errs := make([]error, len(urls))
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", "golang-concurrent-client")
resp, err := client.Do(req)
if err != nil {
errs[idx] = err
return
}
defer resp.Body.Close()
// 处理响应...
if resp.StatusCode != 200 {
errs[idx] = fmt.Errorf("HTTP %d for %s", resp.StatusCode, u)
}
}(i, url)
}
wg.Wait()
return errs}
什么时候该用 semaphore 控制并发数?
上面例子会一次性启动所有 goroutine,如果 urls 有上万条,可能瞬间打爆本地文件描述符或远端服务。这时需要硬性限流 —— 用带缓冲的 channel 模拟信号量是最简单可靠的方式。
注意:信号量粒度是“正在执行的请求数”,不是“已启动的 goroutine 数”。缓冲大小即最大并发数,比如设为 20,就永远只有最多 20 个请求在跑。
- 别用
time.Sleep代替限流,它不解决资源竞争,只掩盖问题 - 若需动态调整并发数(如根据响应延迟自适应),得换用更重的库如
golang.org/x/sync/semaphore - 错误处理仍要保留:失败的请求不能占用信号量槽位,必须
defer sem.Release(1)
为什么 context.WithTimeout 比 client.Timeout 更可靠?
http.Client.Timeout 只控制整个请求生命周期(从 Do() 开始到响应结束),但不覆盖 DNS 解析、TLS 握手等前期阶段。而 context.WithTimeout 能真正中断阻塞在系统调用(如 connect())上的 goroutine。
实操中应优先用 context 控制超时,尤其在批量请求场景下,避免个别慢请求拖垮整体进度。
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel()req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req) // 此处 err 可能是 context.DeadlineExceeded if err != nil { if errors.Is(err, context.DeadlineExceeded) { // 明确知道是超时,可做特殊处理 } }
并发网络请求真正的难点不在“怎么发”,而在“怎么控”:控连接、控数量、控超时、控错误传播。很多线上事故源于 Transport 参数沿用默认值,或 goroutine 泄漏后堆积 fd。写完记得用 lsof -p $(pidof yourapp) 看下打开的 socket 数,再压测几轮 —— 实际表现往往和本地小数据测试差很远。










