Go http.Client 实战:连接池复用、超时设置与 context 取消

发布时间:2026/7/23 22:49:23
Go http.Client 实战:连接池复用、超时设置与 context 取消 Go http.Client 实战:连接池复用、超时设置与 context 取消服务上线后跑一段时间,监控告警「文件描述符耗尽」,或者下游一慢,你的服务就跟着雪崩、goroutine 疯涨。翻代码一看,罪魁祸首往往是同一个:http.Get()用得太随意。Go 标准库的http.Client看着简单,但用不对会有两个致命问题——连接不复用和没有超时。这篇把正确姿势讲清楚。坑一:每次请求都新建 Client,连接根本没复用很多人这么写,尤其是封装工具函数时:// ❌ 每次调用都 new 一个 Clientfuncfetch(urlstring)([]byte,error){client:http.Client{}// 每次新建resp,err:client.Get(url)iferr!nil{returnnil,err}deferresp.Body.Close()returnio.ReadAll(resp.Body)}问题不在http.Client本身(它很轻),而在它底层的Transport。连接池是挂在Transport上的,每次新建 Client 就用了一个全新的 Transport,前一次建立的 TCP 连接完全没法复用,每个请求都要重新三次握手、重新 TLS 协商。高并发下连接建了又关、关了又建,TIME_WAIT 堆积,最终 fd 耗尽。http.Get()直接用的是http.DefaultClient,同样有个隐患:它没有超时。正确姿势:全局复用一个 Client,连接池自动生效http.Client是并发安全的,应该创建一个,全局复用:varhttpClienthttp.Client{Timeout:10*time.Second,// 整个请求的兜底超时,必须设Transport:http.Transport{MaxIdleConns:100,// 全局最大空闲连接数MaxIdleConnsPerHost:10,// 每个 host 的空闲连接数(默认才 2,常需调大)IdleConnTimeout:90*time.Second,// 空闲连接多久后关闭},}funcfetch(urlstring)([]byte,error){resp,err:httpClient.Get(url)iferr!nil{returnnil,err}deferresp.Body.Close()returnio.ReadAll(resp.Body)}复用同一个 Client,同一个 host 的请求就能命中空闲连接池,省掉握手开销。MaxIdleConnsPerHost默认只有 2,如果你的服务频繁调用同一个下游,一定要调大,否则超过 2 的连接用完即关,复用率极低。坑二:必须把 Body 读完再 Close,否则连接不回收这是极隐蔽的一个点:只 Close 不读完 Body,连接不会被放回池子里复用。resp,err:httpClient.Get(url)iferr!nil{returnerr}deferresp.Body.Close()// 假如你只看状态码,不读 body:ifresp.StatusCode!200{returnfmt.Errorf(bad status)// ❌ body 没读完就返回,连接被丢弃}底层的连接复用要求 Body 被读到 EOF。没读完的连接会被直接关闭而不是回收。稳妥做法是在丢弃前把剩余内容排空:deferfunc(){io.Copy(io.Discard,resp.Body)// 排空剩余数据,让连接能复用resp.Body.Close()}()超时要分层:Timeout 之外还要用 contextClient.Timeout是整个请求(连接发送读完 Body)的总超时,是个粗粒度兜底。但真实场景常需要针对单次调用设不同超时,或者上游取消时把请求也一起取消——这靠context:funcfetchWithCtx(ctx context.Context,urlstring)([]byte,error){// 给这一次请求单独设 3s 超时ctx,cancel:context.WithTimeout(ctx,3*time.Second)defercancel()req,err:http.NewRequestWithContext(ctx,http.MethodGet,url,nil)iferr!nil{returnnil,err}resp,err:httpClient.Do(req)iferr!nil{returnnil,err// 超时或被取消时,这里返回 context deadline exceeded}deferresp.Body.Close()returnio.ReadAll(resp.Body)}用context的好处是取消会向下传播:如果这个请求是处理某个用户请求时发起的,用户断开连接、上游 context 被取消,这个下游 HTTP 请求会立刻中止,不会白白占着 goroutine 和连接等到超时。这正是防止「下游慢导致 goroutine 堆积雪崩」的关键。context超时和Client.Timeout可以并存,谁先到谁生效——context管精细控制和取消传播,Client.Timeout管兜底,建议都设。别忘了 defer cancel()context.WithTimeout返回的cancel函数一定要调用,哪怕请求正常完成。不调用会导致 context 关联的定时器资源泄漏,go vet会警告the cancel function is not used。defer cancel()是标准写法,养成肌肉记忆。小结Go 里用http.Client记住这几条:全局复用一个 Client,别每次新建——连接池挂在 Transport 上,新建就等于放弃复用。MaxIdleConnsPerHost默认只有 2,调用同一下游频繁就调大它。Body 要读完再 Close(用io.Copy(io.Discard, ...)排空),否则连接不回收。单次超时和取消传播用context,Client.Timeout做兜底,两者都设;cancel记得defer。一句话记忆点:Client 全局共用、Body 读完再关、超时用 context——这三条决定了你的服务在下游变慢时是优雅降级还是连锁雪崩。

相关新闻

最新新闻

日新闻

周新闻

月新闻