Go并发HTTP请求需控并发、复用连接、防崩溃:自定义http.Client的Transport参数(如MaxIdleConns=200、MaxIdleConnsPerHost=50、IdleConnTimeout=30s),全局复用client实例,并用带缓冲channel(如sem := make(chan struct{}, 20))限制并发数。

Go 处理并发 HTTP 请求,核心不是“能不能并发”,而是“怎么控、怎么复用、怎么不崩”。盲目 go client.Do() 一开几十上百个 goroutine,不出三分钟就遇到 too many open files、goroutine 泄漏、目标服务 429 或超时堆积——这不是并发,是自爆。
怎么用 http.Client 复用连接池,避免反复握手
默认的 http.DefaultClient 虽然自带连接池,但参数保守(比如 MaxIdleConnsPerHost=2),高并发下很快打满,被迫新建连接。必须自定义 Transport:
-
MaxIdleConns设为100~500:控制整个客户端最多保持多少空闲连接 -
MaxIdleConnsPerHost设为50:防止单个域名(如 api.example.com)独占全部连接,压垮对方 -
IdleConnTimeout设为30 * time.Second:空闲连接太久易被中间设备(NAT/防火墙)静默断开,设太长反而导致read: connection reset by peer - 务必保持
DisableKeepAlives: false(默认值),否则连接无法复用
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 10 * time.Second,
}这个 client 实例应全局复用,**绝不要在循环里 new 每次都造一个**——否则 Transport 和底层连接池全白配。
怎么限并发数,不让 goroutine 泛滥或打挂下游
并发 ≠ 越多越好。Linux 默认单进程文件描述符上限常为 1024,每个 HTTP 连接至少占 1 个 fd;200 个并发请求若没复用连接,瞬间打穿上限。用带缓冲 channel 做轻量信号量最直接:
- 声明
sem := make(chan struct{}, 20):最多同时跑 20 个请求 - 每个 goroutine 启动前写入
sem ,满则阻塞 - 无论成功失败,结束前必须
归还槽位(用defer最稳) - 配合
sync.WaitGroup等待全部完成,别靠time.Sleep猜时间
sem := make(chan struct{}, 20)
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
resp, err := client.Get(u)
if err != nil {
log.Printf("GET %s failed: %v", u, err)
return
}
defer resp.Body.Close()
// 处理 body...
}(url)}
wg.Wait()
注意:别把 sem 放在 go 外面——那会提前占满通道,实际没真正启动 goroutine 就卡住。
为什么必须传 context.Context,而不是只靠 client.Timeout
client.Timeout 只控制整个请求生命周期,但无法响应上游取消(比如用户关网页、API 网关超时中断)。若下游响应慢,你的 goroutine 就一直挂着,越积越多。
- 用
context.WithTimeout(r.Context(), 5*time.Second) 把请求上下文往下传
- 构造
*http.Request 时用 http.NewRequestWithContext(ctx, ...)
- 这样一旦父 context 被 cancel,
client.Do() 会立即返回 context.Canceled 错误,goroutine 快速退出
- 尤其在代理类服务、链路较长的微服务调用中,这是防止雪崩的关键
漏掉这步,压测时可能发现 CPU 不高、内存不涨,但 goroutine 数持续上升——典型的 context 泄漏。
错误处理不能只靠 log,要收敛、要可观测
并发中每个 goroutine 的错误是孤立的,log.Printf 会混成一团,无法统计失败率或定位哪批 URL 出问题。
- 用带缓冲的
errChan := make(chan error, len(urls)) 收集错误(缓冲大小 ≥ 并发上限)
- 每个 goroutine 失败时
errChan ,成功也建议发个 nil 便于计数
- 主 goroutine
close(errChan) 后遍历,可统计成功率、聚合错误类型
- 更进一步:用
errgroup.Group 替代手写 channel,自动支持 context 取消和错误传播
真正的难点不在“怎么发”,而在“发崩了怎么知道”——没错误收敛机制,线上出问题只能翻日志大海捞针。
连接复用、并发控制、context 传递、错误收敛,四者缺一不可。少配一个,高并发下大概率不是性能差,而是直接不可用。










