在 Go 语言里写 HTTP 请求,哪怕是最资深的工程师也大概率踩过同一个暗坑:调完接口只检查了一个状态码,随手写上一行 defer resp.Body.Close(),底层的 TCP 连接其实就已经被悄悄扔进了垃圾桶。
2026 年 10 月 14 日,Go 官方博客刊发了 Nicholas Husin 和 Damien Neil 撰写的文章,宣布 Go 1.27 的标准库 net/http 彻底修正了这个困扰开发者十余年的隐蔽性能陷阱。在官方测试中,连续 100 次向 index.golang.org 发送仅检查状态码的 HTTP/1 请求,耗时从 Go 1.26 的 约 710 毫秒 骤降至 Go 1.27 的 约 150 毫秒,整整提速了近 5 倍。
这一底层优化的核心代码落在 CL 737720 与 CL 741222(关联 Issue #77370),全程没有新增任何暴露给开发者的 API。
为什么一个未读的 Body 会让性能垮掉
HTTP/1.1 虽然默认开启了持久连接,但它不支持基于帧的多路复用。请求和响应在 TCP 管道里必须严格排队串行收发,客户端想要知道下一个响应从哪开始,前提是把当前响应的数据流一字不差地读到末尾(EOF)。

当你在代码里只看了一眼 resp.StatusCode 就调用 resp.Body.Close(),Go 1.26 及更早版本的标准库会发现数据还没读完。为了防止后续请求把残留数据当成新的响应头读进来,底层的 Transport 别无选择,只能将当前 TCP 连接强行关闭。
每一次断开,意味着下一个请求必须从头支付网络通信的物理开销:
- 1 次 RTT 的 TCP 三次握手;
- 1 到 2 次 RTT 的 TLS 握手与证书交换;
- 随后才能正式发送 HTTP 请求报文。
在跨地域或高延迟网络中,一次握手耗时轻松突破几十毫秒。真正传输业务数据可能只要几毫秒,大半的时间全耗费在重复建立连接上。这种静默降级最可怕的地方在于它不会产生报错,所有接口都能正常返回,只有机器的 CPU 占用和请求耗时在暗中飙升。哪怕是用 encoding/json 反序列化,只要服务端在 JSON 对象末尾多送了一个换行符,解码器停在最后一个大括号处,也会导致连接无法复用。
256 KiB 与 50 毫秒的双重防火墙
过去很多工程团队为了应对这个问题,会在业务代码里硬塞一层处理:在关闭前调用 io.Copy(io.Discard, resp.Body)。

这种手写排空的做法表面上保住了连接,实则埋下了更大的雷。一旦服务端吐出数以百兆计的文件流,或者下游服务陷入慢速滴灌,客户端就会傻傻地在主流程里无限期读取并丢弃数据,直接把整个服务的线程拖死。
Go 1.27 的解法非常克制,在标准库内部实现了一套带预算的后台排空机制:
真正的工程优雅不是把所有残渣都吞下去,而是给垃圾清理设下严格的止损边界。
首先,resp.Body.Close() 本身变成了一个非阻塞调用,调用方完全感知不到延迟,排空逻辑被扔进了独立的后台协程中。
其次,标准库设立了极其硬性的双重安全线:最多只读取 256 KiB 数据,或者最多消耗 50 毫秒 耗时。任何一个条件触发且未见 EOF,标准库就会立即终止排空并断开连接。如果响应头里带了 Connection: close,由于连接本就注定关闭,排空流程甚至不会被触发。
值得注意的一个细节是,排空资格的判定依据是响应头中声明的总 ContentLength。如果一个文件下载接口声明的 ContentLength 达到了 10 MiB,即使你的程序已经读掉了 9.99 MiB,只剩下最后几字节没读就调了 Close,Go 1.27 也不会触发自动排空,这条连接依然会被丢弃(只有未声明长度的 chunked 传输等 -1 情况会例外放行)。
并非无代价:谁受益,谁该警惕
很多开发者看到官方跑分飙升,就以为无脑升级到 Go 1.27 就能白拿 5 倍性能,事实并非如此。

在排空进行的这最多 50 毫秒之内,该底层连接一直处于被占用的繁忙状态,并没有回到闲置连接池(Idle Pool)中。在极端高并发的瞬间流量下,紧随其后的请求并不能立刻复用它,依然可能触发新建连接。
- 建议.立即排查代码中遗留的手写
io.Copy(io.Discard, resp.Body)逻辑。这些缺乏超时限制的手写排空不仅冗余,在高版本标准库面前更是多余的风险敞口。
- 风险.若在配置中设置了
MaxIdleConns = 0,或者频繁为每个请求创建独立的http.Client与底层Transport,导致连接池无法留存闲置连接,Go 1.27 的后台排空不仅无法带来任何复用收益,反而白白烧掉了额外的带宽与 CPU 周期。
从跨语言视角来看,处理这个网络遗留问题各有各的取舍。Python 的 Requests 默认将响应体完整拉进内存,以占用内存为代价换取了复用;Java HttpClient 的流模式一旦提前关闭,直接发出取消信号抛弃底层连接,把责任推给调用者。
Go 1.27 没有修改任何 API,用一种近乎暗度陈仓的防御性工程设计,替开发者在“保连接”与“防拖垮”之间拉起了一根精密的平衡木。
