Go 部落格
Go 1.22 路由增強
Go 1.22 為 net/http
包的路由器帶來了兩項增強:方法匹配和萬用字元。這些功能允許您使用模式而不是 Go 程式碼來表達常用路由。儘管它們易於解釋和使用,但在多個模式匹配請求時,找出選擇獲勝模式的正確規則卻是一個挑戰。
我們進行這些更改是為了繼續努力使 Go 成為構建生產系統的優秀語言。我們研究了許多第三方 Web 框架,提取了我們認為最常用的功能,並將它們整合到 net/http
中。然後,我們透過在 GitHub 討論和 提案 issue 中與社群合作,驗證了我們的選擇並改進了我們的設計。將這些功能新增到標準庫意味著許多專案可以減少一個依賴項。但對於現有使用者或具有高階路由需求的專案,第三方 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)
此模式匹配一個 GET 請求,其路徑以“/posts/”開頭,幷包含兩個段。(特殊情況下,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 請求,而後者匹配任何方法的請求。
“最具體獲勝”規則概括了原始模式(沒有萬用字元或 {$}
)的路徑部分的原始“最長獲勝”規則。這類模式僅在一個是另一個的字首時重疊,並且更長的模式更具體。
如果兩個模式重疊但 neither is more specific 怎麼辦?例如,/posts/{id}
和 /{resource}/latest
都匹配 /posts/latest
。對於哪一個優先,沒有明確的答案,因此我們認為這些模式相互衝突。註冊兩者(無論順序如何!)都會導致 panic。
優先順序規則對於方法和路徑的工作方式與上面完全相同,但為了保持相容性,我們必須對主機做出一個例外:如果兩個模式原本會衝突,並且其中一個有主機而另一個沒有,則帶有主機的模式優先。
計算機科學專業的學生可能會回想起正則表示式和正則語言的美麗理論。每個正則表示式都會選擇一個正則語言,即表示式匹配的字串集。有些問題透過討論語言而不是表示式來提出和回答會更容易。我們的優先順序規則受到了該理論的啟發。事實上,每個路由模式都對應一個正則表示式,而匹配請求的集合扮演著正則語言的角色。
透過語言而不是表示式定義優先順序,可以輕鬆陳述和理解。但是,有一個基於可能無限集合的規則的缺點:不清楚如何高效地實現它。事實證明,我們可以透過逐段遍歷來確定兩個模式是否衝突。粗略地說,如果一個模式在另一個模式有萬用字元的地方有一個字面量段,那麼它就更具體;但如果字面量在兩個方向上都與萬用字元對齊,則模式會衝突。
當新的模式被註冊到 ServeMux
時,它會檢查與先前註冊模式的衝突。但是檢查每對模式將花費二次時間。我們使用索引來跳過不能與新模式衝突的模式;實際上,這效果很好。無論如何,此檢查發生在模式註冊時,通常在伺服器啟動時。Go 1.22 中匹配傳入請求的時間與之前版本相比變化不大。
相容性
我們盡了一切努力使新功能與舊版本的 Go 保持相容。新的模式語法是舊語法的超集,新的優先順序規則是對舊規則的泛化。但有一些邊緣情況。例如,Go 的先前版本接受帶有花括號的模式並將它們視為字面量,但 Go 1.22 使用花括號表示萬用字元。GODEBUG 設定 httpmuxgo121
可恢復舊行為。
有關這些路由增強的更多詳細資訊,請參閱 net/http.ServeMux
文件。
下一篇文章: 健壯的切片泛型函式
上一篇文章: Go 1.22 釋出!
部落格索引