根本原因是http.defaultclient的transport默认maxidleconnsperhost=2,导致同一域名最多复用2个空闲连接,超并发请求被迫排队或新建连接,引发卡顿、超时及fd耗尽;必须自定义client并合理配置maxidleconns、maxidleconnsperhost、idleconntimeout等参数,配合context超时与并发控制。

goroutine并发请求时为什么HTTP连接会卡住
根本原因是http.Client默认的Transport参数太保守:MaxIdleConnsPerHost默认为2,意味着每个域名最多复用2个空闲连接。一旦并发请求数超过2,后续请求就得排队等空闲连接,表现就是“卡在半路”或超时。
必须显式配置Transport,否则哪怕只开10个goroutine,也大概率被限速:
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100, // ⚠️ 这行不设等于白配
IdleConnTimeout: 30 * time.Second,
},
}
-
MaxIdleConns控制全局空闲连接总数,MaxIdleConnsPerHost才是每域名上限,两者都得设 - 不设
Timeout会导致失败请求永久阻塞goroutine,最终耗尽资源 - 不要每次请求都新建
http.Client——它内部有连接池,重建等于放弃复用
for循环里直接go http.Get为什么会串行执行
典型错误写法:wg.Wait()写在循环体内,导致每个goroutine启动后立刻被阻塞等待完成,整个流程退化为串行。
正确结构是:先批量go启动所有goroutine,最后统一wg.Wait():
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := client.Get(u)
if err != nil {
log.Printf("failed %s: %v", u, err)
return
}
defer resp.Body.Close() // ⚠️ 必须关,否则fd泄漏
// 处理resp
}(url) // ⚠️ 传参防闭包捕获循环变量
}
wg.Wait()
- 闭包中直接引用
url(未传参)会导致所有goroutine实际请求最后一个url值 -
defer resp.Body.Close()不能省,高并发下不关会快速触发too many open files - 如果urls切片很大,上面写法会一次性启动全部goroutine,可能压垮客户端或服务端
如何安全限制并发数:用channel当信号量
用带缓冲的chan struct{}是最轻量、最可控的限流方式,容量即最大并发数。
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
- 容量设为20,不代表只能发20次请求——而是同一时刻最多20个请求在跑,其余排队等令牌
defer func() { 必须这么写,如果写成<code>defer 会立即执行,起不到延迟释放作用- 别用
time.Sleep或runtime.Gosched()替代,它们不可控、难调试
context超时和错误net/http: request canceled怎么区分
context.DeadlineExceeded是你主动设了context.WithTimeout且到期了;而net/http: request canceled更可能是父context被cancel(比如上层函数return了),或client被显式关闭。
推荐统一用子context管理单个请求超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
- 不要混用
client.Timeout和context.WithTimeout,优先用后者,粒度更细 - 如果用
http.Get,它内部用的是context.Background(),无法控制单请求超时,必须换client.Do(req) -
request canceled错误出现时,先检查是否误调用了cancel(),或父context提前结束
真正的难点不在“怎么开goroutine”,而在“怎么不让它失控”:连接池参数、令牌归还时机、闭包变量捕获、响应体关闭——每一处漏掉,都会在流量上来时突然崩掉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











