
本文详解 Go 语言中 http.Get 在高频、多域名场景下因默认启用 HTTP Keep-Alive 而引发的内存泄漏问题,介绍如何通过禁用连接复用、复用 http.Client 实例及正确释放资源来彻底解决内存持续增长问题。
本文详解 go 语言中 `http.get` 在高频、多域名场景下因默认启用 http keep-alive 而引发的内存泄漏问题,介绍如何通过禁用连接复用、复用 `http.client` 实例及正确释放资源来彻底解决内存持续增长问题。
在使用 Go 编写网络爬虫或批量 URL 检测工具时,开发者常误以为调用 resp.Body.Close() 即可完全释放请求所占内存。但实际运行中,程序内存占用却随请求数线性上升,最终触发 Linux OOM Killer —— 这并非 GC 失效,而是 net/http.Transport 的连接池机制在“默默囤积”大量空闲连接。
根本原因在于:Go 的默认 http.DefaultClient 使用 http.DefaultTransport,其 KeepAlive 默认开启(DisableKeepAlives: false)。当程序访问成千上万个不同域名(如 example1.com、example2.net …)时,Transport 会为每个新主机缓存一个空闲连接(含 bufio.Reader/Writer 及底层 socket),而这些连接因极少复用,长期滞留在 idleConn map 中,无法被及时回收。pprof 输出中 bufio.NewReaderSize 和 net/http.(*Transport).getIdleConnCh 的高占比正是此现象的直接证据。
✅ 正确解法是 显式配置 Transport 并禁用 Keep-Alive,同时确保 http.Client 实例复用(避免每 goroutine 创建新 client)。以下是优化后的核心 worker 实现:
func worker(linkChan chan string, wg *sync.WaitGroup) {
defer wg.Done()
// ✅ 复用 Transport + 禁用 Keep-Alive,杜绝跨域名连接堆积
transport := &http.Transport{
DisableKeepAlives: true,
// 可选:限制最大空闲连接数(即使 Keep-Alive 关闭也建议设为合理值)
MaxIdleConns: 10,
MaxIdleConnsPerHost: 10,
}
client := &http.Client{Transport: transport}
for url := range linkChan {
resp, err := client.Get(url)
if err != nil {
fmt.Printf("Fail url: %s\n", url)
continue
}
// ✅ 必须读取并关闭 Body,否则连接可能无法释放(即使 DisableKeepAlives)
body, err := io.ReadAll(resp.Body)
resp.Body.Close() // 关键:防止 reader 泄露
if err != nil {
fmt.Printf("Read error for %s: %v\n", url, err)
continue
}
hasRemCode := strings.Contains(string(body), "googleadservices.com/pagead/conversion.js")
fmt.Printf("Done url: %s\t%t\n", url, hasRemCode)
}
}⚠️ 注意事项:
- DisableKeepAlives: true 是解决海量异构域名场景内存膨胀的最直接手段;若目标站点集中(如仅爬取同一 CDN 下的子域),可保留 Keep-Alive 并调优 MaxIdleConnsPerHost。
- 切勿在循环内创建 http.Client 或 http.Transport 实例 —— 它们本就是并发安全且设计为长生命周期复用的。
- ioutil.ReadAll(Go 1.16+ 已迁移至 io.ReadAll)会将整个响应体加载至内存,对大页面存在风险;如只需部分匹配,建议使用流式处理(如 bufio.Scanner)。
- defer resp.Body.Close() 在循环内不推荐:延迟执行会累积 goroutine 栈帧,应改用立即调用。
? 总结:Go 的 HTTP 客户端内存管理高度依赖 Transport 配置。面对分布式、多源 URL 场景,主动关闭 Keep-Alive + 复用 Client + 及时关闭 Body,三者缺一不可。这不仅是内存优化技巧,更是理解 Go HTTP 底层连接生命周期的关键实践。










