Go 部落格
Go 1.22 的路由增強功能
Go 1.22 為 `net/http` 包的路由器帶來了兩項增強功能:方法匹配和萬用字元。這些特性讓您可以將常見路由表示為模式而非 Go 程式碼。儘管它們易於解釋和使用,但當多個模式匹配一個請求時,如何選擇獲勝模式的規則卻是一個挑戰。
我們進行了這些更改,作為持續努力使 Go 成為構建生產系統優秀語言的一部分。我們研究了許多第三方 Web 框架,提取了我們認為最常用的特性,並將它們整合到 `net/http` 中。然後,我們透過在 GitHub 討論和提案議題中與社群協作,驗證了我們的選擇並改進了設計。將這些特性新增到標準庫意味著許多專案可以減少一個依賴。但對於現有使用者或具有高階路由需求的程式來說,第三方 Web 框架仍然是一個不錯的選擇。
增強功能
新的路由特性幾乎完全影響傳遞給兩個 `net/http.ServeMux` 方法 `Handle` 和 `HandleFunc` 以及相應的頂層函式 `http.Handle` 和 `http.HandleFunc` 的模式字串。唯一的 API 更改是 `net/http.Request` 上用於處理萬用字元匹配的兩個新方法。
我們將透過一個假定的部落格伺服器來演示這些更改,其中每個帖子都有一個整數識別符號。像 `GET /posts/234` 這樣的請求會檢索 ID 為 234 的帖子。在 Go 1.22 之前,處理這些請求的程式碼會以這樣一行開頭
http.HandleFunc("/posts/", handlePost)
末尾的斜槓將所有以 `/posts/` 開頭的請求路由到 `handlePost` 函式,該函式必須檢查 HTTP 方法是否為 GET,提取識別符號,並檢索帖子。由於方法檢查對於滿足請求並非嚴格必需,因此省略它是一個很自然的錯誤。這意味著像 `DELETE /posts/234` 這樣的請求也會獲取帖子,這至少令人感到意外。
在 Go 1.22 中,現有程式碼將繼續工作,或者您可以改寫成這樣
http.HandleFunc("GET /posts/{id}", handlePost2)
此模式匹配路徑以“/posts/”開頭並具有兩個段的 GET 請求。(作為特例,GET 也匹配 HEAD;所有其他方法則精確匹配。) `handlePost2` 函式不再需要檢查方法,並且可以使用 `Request` 上的新 `PathValue` 方法編寫提取識別符號字串的程式碼
idString := req.PathValue("id")
`handlePost2` 的其餘部分將像 `handlePost` 一樣工作,將字串識別符號轉換為整數並獲取帖子。
如果未註冊其他匹配模式,像 `DELETE /posts/234` 這樣的請求將會失敗。根據 HTTP 語義,`net/http` 伺服器將回復此類請求,並返回 `405 Method Not Allowed` 錯誤,並在 `Allow` 頭中列出可用方法。
萬用字元可以匹配整個段,例如上面示例中的 `{id}`,或者如果它以 `...` 結尾,則可以匹配路徑中所有剩餘的段,就像 `/files/{pathname...}` 模式中那樣。
還有最後一點語法。如上所示,以斜槓結尾的模式(例如 `/posts/`)匹配所有以該字串開頭的路徑。要僅匹配帶有末尾斜槓的路徑,可以寫 `/posts/{$}`。這將匹配 `/posts/`,但不匹配 `/posts` 或 `/posts/234`。
還有最後一個 API:`net/http.Request` 有一個 `SetPathValue` 方法,以便標準庫之外的路由器可以透過 `Request.PathValue` 提供其自己的路徑解析結果。
優先順序
每個 HTTP 路由器都必須處理重疊模式,例如 `/posts/{id}` 和 `/posts/latest`。這兩個模式都匹配路徑“posts/latest”,但最多隻有一個可以處理請求。哪個模式優先?
有些路由器不允許重疊;有些則使用最後註冊的模式。Go 始終允許重疊,並且無論註冊順序如何,都會選擇更長的模式。保持順序無關性對我們很重要(並且為了向後相容也是必需的),但我們需要一個比“最長者獲勝”更好的規則。那個規則會選擇 `/posts/latest` 而不是 `/posts/{id}`,但會同時選擇 `/posts/{identifier}` 而不是兩者。這看起來是錯誤的:萬用字元名稱不應該重要。感覺上 `/posts/latest` 應該始終贏得這場競爭,因為它匹配的是單一路徑而非許多路徑。
我們尋求一個好的優先順序規則,這讓我們考慮了模式的許多屬性。例如,我們考慮優先選擇具有最長字面量(非萬用字元)字首的模式。這將選擇 `/posts/latest` 而不是 `/posts/ {id}`。但它無法區分 `/users/{u}/posts/latest` 和 `/users/{u}/posts/{id}`,而看起來前者應該優先。
我們最終選擇了一條基於模式含義而非外觀的規則。每個有效模式都匹配一組請求。例如,`/posts/latest` 匹配路徑為 `/posts/latest` 的請求,而 `/posts/{id}` 匹配第一段為“posts”的任何兩段路徑的請求。如果一個模式匹配的是另一個模式匹配請求的嚴格子集,我們就說前者比後者更具體。模式 `/posts/latest` 比 `/posts/{id}` 更具體,因為後者匹配前者匹配的所有請求,而且更多。
優先順序規則很簡單:最具體的模式獲勝。這條規則與我們的直覺相符,即 `posts/latests` 應該優先於 `posts/{id}`,而 `/users/{u}/posts/latest` 應該優先於 `/users/{u}/posts/{id}`。對於方法來說也同樣適用。例如,`GET /posts/{id}` 優先於 `/posts/{id}`,因為前者只匹配 GET 和 HEAD 請求,而後者匹配任意方法的請求。
“最具體者獲勝”規則推廣了原始模式(沒有萬用字元或 `{$}` 的模式)路徑部分的原始“最長者獲勝”規則。此類模式僅當其中一個為另一個的字首時才會重疊,而較長的那個更具體。
如果兩個模式重疊但都沒有更具體呢?例如,`/posts/{id}` 和 `/{resource}/latest` 都匹配 `/posts/latest`。哪個優先沒有明確的答案,因此我們認為這些模式相互衝突。註冊其中任何一個(無論順序如何!)都會導致 panic。
優先順序規則對於方法和路徑與上述完全一致,但為了保持相容性,我們對主機做了一個例外:如果兩個模式在其他方面衝突,而其中一個包含主機,另一個不包含,則包含主機的模式優先。
計算機科學的學生可能會回想起正則表示式和正則語言的美妙理論。每個正則表示式對應一種正則語言,即由該表示式匹配的字串集合。透過討論語言而非表示式,一些問題更容易提出和解答。我們的優先順序規則受到了這一理論的啟發。事實上,每個路由模式都對應一個正則表示式,而匹配請求的集合扮演著正則語言的角色。
透過語言而非表示式來定義優先順序使其易於闡述和理解。但基於可能無限集合的規則有一個缺點:如何高效地實現它並不清楚。事實證明,我們可以透過逐段遍歷模式來確定兩個模式是否衝突。粗略地說,如果一個模式在另一個模式是萬用字元的地方有一個字面量段,那麼它就更具體;但如果字面量與萬用字元在兩個方向上都對齊,則模式衝突。
當新模式在 `ServeMux` 上註冊時,它會檢查與先前註冊模式的衝突。但檢查每一對模式將花費二次時間。我們使用索引來跳過不會與新模式衝突的模式;在實踐中,這效果很好。無論如何,這個檢查發生在模式註冊時,通常在伺服器啟動時。Go 1.22 中匹配傳入請求所需的時間與之前版本相比變化不大。
相容性
我們盡一切努力使新功能與舊版本 Go 相容。新模式語法是舊模式語法的超集,新優先順序規則也推廣了舊規則。但也有一些邊緣情況。例如,Go 的先前版本接受帶大括號的模式並將其視為字面量,但 Go 1.22 使用大括號表示萬用字元。GODEBUG 設定 `httpmuxgo121` 可以恢復舊的行為。
有關這些路由增強功能的更多詳細資訊,請參閱 `net/http.ServeMux` 文件。
下一篇文章:切片上的健壯泛型函式
上一篇文章:Go 1.22 釋出了!
部落格索引