http.defaultclient一并发就卡死,因其transport默认maxidleconnsperhost=2、idleconntimeout=0、未设timeout,导致连接复用率极低、dns/tls无限等待、fd耗尽;必须自定义client并为每个goroutine单独创建带cancel的context,配合worker池或semaphore控并发。

http.DefaultClient 为什么一并发就卡死
直接用 http.Get() 并发几十个请求,大概率出现部分请求永远不返回、程序卡住、甚至 DNS 解析全阻塞——不是 Goroutine 有问题,而是 http.DefaultClient 的底层 Transport 默认配置太保守:MaxIdleConnsPerHost 是 2,IdleConnTimeout 为 0,Timeout 完全没设。
- 没设
Timeout→ DNS 查询、TLS 握手、读响应头都可能无限等待 -
MaxIdleConnsPerHost=2→ 同一域名最多复用 2 个空闲连接,其余请求排队等连接,实际变成串行 - 没设
IdleConnTimeout→ 空闲连接永不释放,文件描述符耗尽后新请求失败
必须显式构造自定义 http.Client,否则并发数上去就是自伤。
每个请求都要独立的 context.WithTimeout
外层只套一次 context.WithTimeout 然后传给所有 goroutine,看似省事,实则让整个并发退化成“第一个超时,全部取消”。真正有效的做法是:在每个 goroutine 内部创建自己的上下文。
- 正确写法:
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)放在 goroutine 函数体开头 - 务必调用
cancel()(通常 defer),否则 context 泄漏,goroutine 不会自动退出 - 不能把同一个
ctx传给多个请求 —— 它们共享取消信号,失去并发意义
控制并发数比堆 goroutine 数量更重要
无节制地 go fetch(url) 很快打爆本地 fd 限制或目标服务限流,反而降低吞吐。稳定高并发靠的是可控的 worker 池,不是数量堆砌。
- 用带缓冲的 channel 做任务队列(如
jobs := make(chan string, 100)),再起固定数量 worker(比如 20 个)从 channel 取任务 - 或用
semaphore(如golang.org/x/sync/semaphore)限制同时活跃的请求数 - 缓冲 channel 容量建议 ≥ 并发 worker 数,避免发送端阻塞导致 goroutine 积压
- 别依赖
sync.WaitGroup+ 无缓冲 channel 组合——接收端稍慢,发送端就会 hang 住
结果收集别用无缓冲 channel 直接传 struct
像 results := make(chan Result) 这种无缓冲 channel,一旦接收方处理稍慢,所有发送 goroutine 就卡在 results 上,内存和 goroutine 都涨不停。
- 改用带缓冲 channel:
results := make(chan Result, 50),容量匹配预期并发上限 - 或改用
sync.WaitGroup+ 闭包捕获变量,把结果 append 到切片(注意加锁或用sync.Map) - 记得在所有 worker 启动完后,另起一个 goroutine 调用
wg.Wait()然后close(results),否则for r := range results永不结束
真正难的不是启动多少 goroutine,而是让它们不互相拖垮、不泄漏资源、不把错误扩散成雪崩。超时、连接池、上下文隔离、缓冲通道——这四样缺一不可,少一个,跑得越快,崩得越彻底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











