Go 1.27 讓 HTTP/1 關閉未讀 body 時也能重用連線
go.dev 官方部落格(Nicholas Husin、Damien Neil) · 2026-10-14
Go 的 net/http 在 HTTP/1 下,過去只要 resp.Body.Close() 時 body 還沒讀完,就會直接丟掉整條連線;Go 1.27 改成在背景把剩下的內容排空,讓連線回到 idle pool 重用。官方在 部落格與 Go 1.27 release notes 都說明了這項變更。
原本的問題
HTTP/1 的同一條連線上,回應是依序送回的。要重用連線,前一個回應的 body 必須先讀到結尾;Go 1.26 以前,若 body 沒讀完就關閉,net/http 只能放棄這條連線,下一個請求得重新建連線。
建立新連線的成本不低:部落格指出,TLS 1.2 下約需 3 個 RTT(TCP 1 個、TLS 1.2 握手 2 個,再加請求本身),TLS 1.3 的握手則是 1 個 RTT。HTTP/2 因為多工,沒讀完的 body 不會擋住其他請求,所以這個問題只出現在 HTTP/1。
實務上最常踩到的有兩種情況:
- 只關心狀態碼、忽略 body 的呼叫端,例如 body 只是一段小小的 JSON 狀態訊息。
encoding/json.Decoder這類解碼器,解完 JSON 後停在結尾換行字元之前,剩下一個位元組沒讀,就讓連線報廢。
舊的繞路寫法
為了保住連線,開發者得自己在 Close 前排空 body。部落格列出了演進過程:先直接排空,再限制最多 256 KB 與 50 ms,最後改成背景非同步排空,並指出這很難寫對,而且對 HTTP/2 來說是多餘的。
// Go 1.26 以前常見的繞路寫法(示意)
defer func() {
io.Copy(io.Discard, resp.Body) // 沒有上限,可能卡很久
resp.Body.Close()
}()Go 1.27 的做法
從 Go 1.27 起,net/http.Transport 會在關閉 HTTP/1 的 Response.Body 時,自動非同步排空未讀內容,上限為 50 ms 或 256 KB。若回應帶有 Connection: close,連線本來就會被關閉,排空只是白費工夫,因此跳過。官方也註明這個行為未來可能調整。
| Go 1.26 以前 | Go 1.27 | |
|---|---|---|
| 關閉未讀完的 HTTP/1 body | 放棄連線 | 背景排空後放回 idle pool |
| 排空上限 | 無(需自行實作) | 50 ms / 256 KB |
Connection: close | 關閉連線 | 不排空,關閉連線 |
部落格的基準測試是:在 GODEBUG=http2client=0(強制 HTTP/1)下,連續 100 次對 index.golang.org 發 GET 並回傳狀態碼,Go 1.26 約 710 ms,Go 1.27 約 150 ms,接近 5 倍。這是作者在自己機器上的單一測試,並非通用保證。
這個差距的來源很直接:Go 1.26 每次呼叫都要付一次新連線的握手成本,Go 1.27 則重用同一條連線,只付請求本身的往返。連線延遲越高(跨區域呼叫、行動網路),省下的時間越明顯,因為成本是以 RTT 為單位計算的。
排空之所以設上限,是因為對方可能回傳一個非常大的 body。為了重用連線而去讀超大的資料,可能比重建連線更慢,所以 50 ms 與 256 KB 兩個條件任一達到就放棄。排空在背景進行,呼叫 Close 的 goroutine 不會被阻塞。
影響範圍
release notes 的評估是:對多數程式而言,這項變更等同無影響或帶來效能提升。要檢查的是這幾類程式:
- 手寫排空邏輯的程式:可以評估拿掉,直接
defer resp.Body.Close(),部落格也建議這樣做。 - 只讀狀態碼的健康檢查、探測器、API client:不改程式碼即可受益。
- 把
Transport.MaxIdleConns設為0,或為每個請求建立不同Client來繞過上限的程式:release notes 指出,這類本來就不依賴連線重用、又讓大量 idle 連線滯留的程式,可能出現效能退化。
若確定不需要連線重用,release notes 的做法是把 Transport.DisableKeepAlives 設為 true;但它也提醒,這種退化通常代表 Transport 或 Client 的用法本身就有問題,值得回頭檢查。
tr := &http.Transport{
DisableKeepAlives: true, // 明確關閉連線重用
}
client := &http.Client{Transport: tr}升級後若要確認效果,可以在 GODEBUG=http2client=0 下對自己的服務重跑類似部落格的連續請求測試,比較兩個版本的耗時;沒有強制 HTTP/1 時,多數 HTTPS 服務會走 HTTP/2,本來就不受這個問題影響。
另外,release notes 與部落格都未公布這項變更對應的 issue 或 CL 編號,本文因此不引用。