絕了,Go HTTP/2 終于要進入標準庫了!!!
絕了。最近 Go 社區有個重磅消息,HTTP/2 終于要正式進入標準庫了!
這事兒說起來挺有意思的。很多同學可能都遇到過這樣的尷尬場景:想配置一下 HTTP/2 的參數,結果發現必須要導入 golang.org/x/net/http2 這個官方的 ”外部“ 包。
圖片
前兩天剛哭訴完 HTTP/3 被擱置。今天我們再來聊聊 Go 團隊為什么終于肯把 HTTP/2 搬進標準庫,以及這背后的故事。
背景:一個別扭的設計
從 Go 1.6 開始,net/http 包就提供了對 HTTP/2 的透明支持。但這個支持方式有點特別——HTTP/2 的真正實現其實在 golang.org/x/net/http2 包里,標準庫只是把它打包進來。
為什么要這么做呢?主要是為了避免循環依賴的問題。http2 包依賴 net/http,而 net/http 又要用 http2,這就形成了一個死循環。
Go 團隊用了一個叫 bundle 的工具,把整個 http2 包合并成一個單獨的文件(h2_bundle.go),然后塞進 net/http 里。
圖片
這個設計在早期是有好處的:
- 可以在 Go 發布周期之外更新 HTTP/2 實現
- 快速迭代,不受兼容性承諾約束
- 用戶可以自己選擇 HTTP/2 的版本
有什么問題?
但現在問題越來越多了。
1、關系太復雜了:net/http、h2_bundle.go、golang.org/x/net/http2 三者之間的關系讓人頭大,新手看了直接懵。
2、修 bug 太麻煩了:每次要給 HTTP/2 打安全補丁,都得在多個版本之間來回折騰,backport 的流程異常復雜。
3、配置不方便:想調個 HTTP/2 參數?對不起,必須導入外部包。而且導入之后,不光配置變了,連實現都換了,這就很尷尬。
4、開發受限:HTTP/1 和 HTTP/2 的代碼分別在不同的倉庫,想要同時改動兩邊的邏輯,幾乎不可能原子化完成。
舉個例子,之前有個 issue #52459,說的是 HTTP/1 和 HTTP/2 都有自己的重試邏輯,導致請求可能被重試多次。
圖片
最簡單的解決方案是搞一個統一的重試循環,但因為代碼分散在兩個倉庫,實現起來困難重重。
提案:是時候搬家了
Go 團隊提出了一個大膽的計劃:把 HTTP/2 徹底搬進標準庫。
圖片
具體來說,就是把 golang.org/x/net/http2 的實現挪到 net/http/internal/http2 里。
這是個內部包(internal),普通用戶不能直接導入。所有新的開發都會在標準庫里進行。
那老的 x/net/http2 包怎么辦呢?
Go 團隊的計劃是這樣的:
- 在過渡期內,繼續給
x/net/http2包提供 bug 修復。 - 更新
x/net/http2,讓它在新版本 Go 里直接調用標準庫的實現。 - 等所有還在維護的 Go 版本都不再使用舊的 vendor 版本后,正式廢棄
x/net/http2。
這個方案很巧妙。既保證了平滑過渡,又能讓新代碼享受到統一實現的好處。
新的配置方式
搬進標準庫之后,最直觀的變化就是配置方式變了。以前我們要這樣寫:
import (
"net/http"
"golang.org/x/net/http2"
)
func main() {
server := &http.Server{
Addr: ":8080",
}
// 必須調用這個函數來配置 HTTP/2
http2.ConfigureServer(server, &http2.Server{
MaxConcurrentStreams: 250,
IdleTimeout: 5 * time.Minute,
})
}現在可以直接在 net/http 里配置了:
import (
"net/http"
"time"
)
func main() {
server := &http.Server{
Addr: ":8080",
HTTP2: http.HTTP2Config{
MaxConcurrentStreams: 250,
MaxDecoderHeaderTableSize: 4096,
MaxEncoderHeaderTableSize: 4096,
MaxReadFrameSize: 16384,
MaxUploadBufferPerConnection: 1 << 20,
MaxUploadBufferPerStream: 1 << 20,
SendPingTimeout: 15 * time.Second,
PingTimeout: 15 * time.Second,
},
}
server.ListenAndServeTLS("cert.pem", "key.pem")
}是不是清爽多了?不用再導入外部包,所有配置都在標準庫里搞定。
客戶端也是一樣的:
transport := &http.Transport{
HTTP2: http.HTTP2Config{
MaxDecoderHeaderTableSize: 4096,
MaxEncoderHeaderTableSize: 4096,
MaxReadFrameSize: 16384,
},
}
client := &http.Client{
Transport: transport,
}接下來我們看下 HTTP2Config 的具體字段。
HTTP2Config 配置詳解
新的 HTTP2Config 結構體統一了服務端和客戶端的配置項,核心字段包括:
type HTTP2Config struct {
// MaxConcurrentStreams 指定對端可以同時打開的流數量
// 對應 HTTP/2 的 SETTINGS_MAX_CONCURRENT_STREAMS
// 如果為 0,默認值至少為 100
MaxConcurrentStreams uint32
// MaxDecoderHeaderTableSize 設置用于解碼 header 的壓縮表大小
// 對應 SETTINGS_HEADER_TABLE_SIZE,默認 4096 字節
MaxDecoderHeaderTableSize uint32
// MaxEncoderHeaderTableSize 設置用于編碼 header 的壓縮表大小上限
// 接收到的 SETTINGS_HEADER_TABLE_SIZE 會被限制在這個值
// 默認 4096 字節
MaxEncoderHeaderTableSize uint32
// MaxReadFrameSize 指定愿意接收的最大幀大小
// 對應 SETTINGS_MAX_FRAME_SIZE
// 有效值在 16k 到 16M 之間,默認值會被使用
MaxReadFrameSize uint32
// MaxUploadBufferPerConnection 是每個連接的初始流控窗口大小
// HTTP/2 規范不允許小于 65535 或大于 2^32-1
MaxUploadBufferPerConnection int32
// MaxUploadBufferPerStream 是每個流的初始流控窗口大小
// HTTP/2 規范不允許大于 2^32-1
MaxUploadBufferPerStream int32
// SendPingTimeout 是在連接空閑時發送 ping 幀做健康檢查的超時時間
// 如果為 0,不進行健康檢查
SendPingTimeout time.Duration
// PingTimeout 是 ping 響應的超時時間,超時后連接會被關閉
// 如果為 0,使用默認值 15 秒
PingTimeout time.Duration
// PermitProhibitedCipherSuites 如果為 true,
// 允許使用 HTTP/2 規范禁止的加密套件
PermitProhibitedCipherSuites bool
// CountError 在發生 HTTP/2 錯誤時被調用
// 用于監控指標統計,比如 expvar 或 Prometheus
// errType 只包含 ASCII 字母
CountError func(errType string)
}這些配置項涵蓋了 HTTP/2 協議的核心參數,從并發控制、緩沖區大小到健康檢查,基本滿足了日常開發的需求。
協議版本選擇
另一個重要的改進是協議版本選擇。以前想禁用 HTTP/2,得這樣寫:
server := &http.Server{
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}這寫法看起來就很別扭,對吧?新的 API 直觀多了:
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.HTTP1}, // 只用 HTTP/1
}想同時支持 HTTP/1 和 HTTP/2,還能指定優先級:
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.HTTP2, http.HTTP1}, // 優先 HTTP/2
}核心觀點在于,新的 Protocols 字段讓協議選擇變得清晰明了。列表中的順序代表了優先級,服務端會按這個順序和客戶端協商使用哪個協議。
如果不設置 Protocols,默認值是 {HTTP2, HTTP1}。但如果你設置了 TLSNextProto 并且里面沒有 "h2" 這個 key,默認值就會變成 {HTTP1}。這樣就保證了向后兼容。
客戶端的用法也是一樣的:
transport := &http.Transport{
Protocols: []http.Protocol{http.HTTP2, http.HTTP1},
}
client := &http.Client{Transport: transport}h2c 支持
還有個值得一提的改進是對 h2c(未加密的 HTTP/2)的支持。
以前想用 h2c,要么得在 DialTLS 里返回未加密連接(這名字就很詭異),要么得用 golang.org/x/net/http2/h2c 包。現在直接用協議選擇就行了:
server := &http.Server{
Addr: ":8080",
Protocols: []http.Protocol{http.UnencryptedHTTP2},
}
server.ListenAndServe() // 注意不是 ListenAndServeTLS客戶端也一樣簡單:
transport := &http.Transport{
Protocols: []http.Protocol{http.UnencryptedHTTP2},
}
client := &http.Client{Transport: transport}
resp, err := client.Get("http://example.com") // http:// 不是 https://簡單來說,Go 團隊新增了一個 UnencryptedHTTP2 常量,專門用來表示未加密的 HTTP/2 協議。
當 Protocols 同時包含 HTTP1 和 UnencryptedHTTP2 時,服務端和客戶端都會支持 RFC 7540 Section 3.2 定義的 Upgrade: h2c 頭。服務端會把帶 Upgrade: h2c 的 HTTP/1 請求升級到 HTTP/2,客戶端也會發送這個頭。
如果只設置了 UnencryptedHTTP2 而沒有 HTTP1,那么客戶端會對 http:// 開頭的 URL 直接使用 HTTP/2。
向后兼容性保障
Go 核心團隊特別注重向后兼容。現有代碼怎么辦?
如果你在用 http2.ConfigureServer 或 http2.ConfigureTransports,代碼還是能正常工作的。這兩個函數會被更新,在新版本 Go 里自動把 HTTP2 加到 Protocols 列表里。
如果你通過設置 TLSNextProto 為空 map 來禁用 HTTP/2,這種做法也繼續有效。
如果你直接用 http2.Server 或 http2.Transport,照樣沒問題。
對于所有已經發布的 golang.org/x/net/http2 版本,行為會保持不變——選擇了這個包的實現,就會用這個包的,覆蓋標準庫的。
但是很無奈的是,新版本的 golang.org/x/net/http2 在遇到支持非 vendor HTTP/2 的 Go 版本時,會直接調用標準庫的實現。除非你用了標準庫不支持的特性(比如 Server.NewWriteScheduler 或 Transport.ConnPool),否則用的就是標準庫版本。
總之,老代碼不會因為這次改動而出問題。
總結
HTTP/2 進入標準庫這事兒,說白了就是一次技術債償還。當年為了快速迭代選擇了折衷方案,現在該還的債總是要還的。
這次改動帶來的好處很明顯:
- 配置更簡單,不用導入外部包就能配置 HTTP/2 參數
- 維護更容易,bug 修復不用到處 backport 了
- 開發更靈活,HTTP/1 和 HTTP/2 可以一起改,不用擔心跨倉庫的依賴問題
- API 更統一,協議選擇、配置方式都變得清晰明了
預計這個改動會在 Go 1.26 落地。

































