正确做法是用带缓冲channel控制并发数、复用http.Client并配置超时与连接池参数,避免资源耗尽;需传参防闭包变量共享,用子context管理超时,90%场景无需替换为fasthttp。

用 goroutine + http.Client 发起并发 HTTP 请求
Go 实现高并发请求的核心不是“开很多 goroutine 就完事”,而是控制并发数、复用连接、避免资源耗尽。直接对每个请求起一个 goroutine 并发调用 http.Get,在量大时会迅速打爆本地端口、触发 dial tcp: too many open files 或服务端限流。
正确做法是:用带缓冲的 channel 控制并发数,复用同一个 http.Client(它本身是并发安全的),并显式设置超时和连接池参数:
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}
- 不新建
http.Client每次请求——它内部有连接池,重建等于放弃复用 -
MaxIdleConnsPerHost必须设,否则默认是 2,高并发下立刻卡住 - 务必设
Timeout,否则失败请求可能永久阻塞 goroutine
用 sync.WaitGroup + chan 控制并发数量
硬起 1000 个 goroutine 去请求,不如用 channel 做信号量,限制同时最多 20 个请求在跑。这是最常用也最不容易出错的节流方式。
示例逻辑:
立即学习“go语言免费学习笔记(深入)”;
sem := make(chan struct{}, 20) // 并发上限 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)
// 处理 resp / err
}(url)
}
wg.Wait()
- channel 容量即最大并发数,比
time.Sleep或runtime.Gosched()可控得多 - 必须用
defer func() { 确保异常时令牌归还,否则 channel 堵死 - 不要把
url写成闭包外变量引用,否则所有 goroutine 共享最后一个值——要传参进去
遇到 context.DeadlineExceeded 或 net/http: request canceled 怎么办
这两个错误本质都是请求被主动中断,但原因不同:context.DeadlineExceeded 是你设了 context 超时;request canceled 更可能是父 context 被 cancel(比如上层函数 return 了但 goroutine 还在跑)或 client 超时触发。
- 检查是否用了
context.WithTimeout且时间太短——尤其批量请求时,单个超时应 ≤ 总体超时 / 并发数 - 避免在 goroutine 中直接用外部函数的
ctx,应传入新派生的子 context:ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second) -
http.Client.Timeout和context超时不要重复设,优先用 context,更灵活
要不要用 fasthttp 替代标准库
如果压测发现标准 net/http 成为瓶颈(比如 QPS 卡在 5k 上不去、GC 压力大),再考虑 fasthttp。它确实快,但代价明显:
- API 不兼容标准库,
*fasthttp.RequestCtx不能直接当http.ResponseWriter用 - 它复用字节切片,要求你不能保存
req.Body()返回的指针到 goroutine 外,否则数据被覆盖 - 社区中间件少,JWT、OpenTracing 等生态支持弱于
net/http
90% 的业务场景,调优 http.Client + 合理控制并发数,比换框架更稳妥。真正卡在 HTTP 层性能的,往往是服务端响应慢或 DNS 解析没做缓存。










