应使用带缓冲的channel或semaphore控制并发数,而非无限制启动goroutine;sync.WaitGroup仅负责等待完成,真正的限速需通过make(chan struct{}, 10)等信号量机制实现。

用 goroutine + WaitGroup 控制并发数,别直接起成百上千个
Go 启动协程成本低,但不加限制地 go request() 一百次,可能瞬间打爆目标接口或本地文件描述符。关键不是“能不能并发”,而是“怎么安全控速”。sync.WaitGroup 负责等待全部完成,而真正控并发得靠带缓冲的 channel 或 semaphore。
- 用
make(chan struct{}, 10)做信号量:每次请求前sem ,结束后,天然限流 10 并发 - 别把
http.Client创建放在 goroutine 里——复用一个实例,否则 DNS 缓存、连接池全失效 -
context.WithTimeout必须设,否则单个失败请求可能卡死整个批次
sem := make(chan struct{}, 5) // 限制 5 并发
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
sem <- struct{}{} // 获取令牌
defer func() { <-sem }() // 释放令牌
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
resp, err := client.Do(req)
if err != nil {
log.Printf("fail %s: %v", u, err)
return
}
resp.Body.Close()
}(url)}
wg.Wait()
http.Transport 不调优 = 并发再高也白搭
默认 http.DefaultClient 的 Transport 对并发极其保守:最多 100 个空闲连接、每 host 2 个连接、30 秒 idle timeout。压测时经常看到大量 net/http: request canceled (Client.Timeout exceeded while awaiting headers),其实不是超时,是连接被复用阻塞了。
-
MaxIdleConns和MaxIdleConnsPerHost至少设到 200+,尤其目标 host 少的时候 -
IdleConnTimeout建议 30–90 秒,太短导致频繁建连,太长占着连接不放 - 加
ForceAttemptHTTP2: true(Go 1.12+ 默认开启,但显式写上更安心)
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 60 * time.Second,
ForceAttemptHTTP2: true,
},
}批量请求失败后怎么重试又不雪崩?用带退避的 Backoff
直接 for-loop 重试 3 次?网络抖动时所有请求在同一毫秒重试,流量翻三倍。得用指数退避(exponential backoff),且每个请求独立退避,避免同步重试。
- 别手写
time.Sleep(time.Second * 1 ——用github.com/cenkalti/backoff/v4更稳 - 只对 5xx 和部分 429 重试,4xx(如 400、401)基本是客户端错,重试无意义
- 重试次数建议 ≤3,总耗时控制在原始 timeout 内,否则拖垮整批
operation := func() error {
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode >= 500 || resp.StatusCode == 429 {
return fmt.Errorf("server error: %d", resp.StatusCode)
}
return nil
}
err := backoff.Retry(operation, backoff.WithContext(
backoff.NewExponentialBackOff(),
ctx,
))
别忽略 DNS 和 TLS 握手开销——本地缓存和复用很重要
高频请求同一域名时,反复解析 DNS、建立 TLS 连接会吃掉大量时间。Go 默认不缓存 DNS 结果(除非启用了 GODEBUG=http2client=0 这类非标方式),TLS 会话复用也依赖 Transport 配置。
立即学习“go语言免费学习笔记(深入)”;
- 用
net.Resolver自建内存缓存 DNS,TTL 按实际服务稳定性设(比如 5 分钟) -
Transport.TLSClientConfig中启用SessionTicketsDisabled: false(默认已是 false,但显式确认) - 如果目标 API 支持 HTTP/2,确保服务端没禁用 ALPN,否则降级 HTTP/1.1 会损失连接复用能力
并发请求不是堆 go 关键字,是平衡 transport 层、网络层、业务错误处理的系统动作。最容易被跳过的其实是 DNS 缓存和 TLS 复用——它们不报错,但让 QPS 卡在 200 上不去,排查时却总先怀疑代码逻辑。










