處理基於 HTTP 的 API 時,通常使用 URL 路由引數(也稱為路由變數)傳遞資料。這些引數是 URL 路由段的一部分。它們通常用於識別 API 操作的資源。
在除了最簡單的 Web 應用程式之外的所有 Web 應用程式中,API 都是使用路由來定義的。將請求對映到 HTTP 處理程式的模式。
作為路由的一部分,我們可能需要定義路由引數:
/products/{slug}
/users/{id}/profile
/{page}
在上面的路由中, 、{slug}和{id}被{page}命名為路由引數。
這些引數可以透過它們的名稱在 HTTP 處理程式中檢索和使用:
func handler(w http.ResponseWriter, r *http.Request) { <font>//從這裡 Get slug, id or page .<i> }
|
在 Go 版本 1.22 之前,標準庫不支援上面看到的命名引數。這使得路由引數的使用變得有點痛苦。
Go 1.22 增強了路由模式,其中包括對命名路由引數的完全支援。
讓我們看看如何使用它們。
定義路由
http.ServeMux 型別上有兩個方法允許你使用這些路由模式定義路由:Handle 和 HandleFunc。
它們的區別僅在於接受的處理程式型別不同。一種方法接受 http.Handler,另一種方法接受具有以下簽名的函式:
func(w http.ResponseWriter, r *http.Request)
在本文中,我們將始終使用 http.HandleFunc,因為它更簡潔。
萬用字元
如果檢視文件,沒有提到 "路由引數",但有一個稍微寬泛的概念:萬用字元。
萬用字元允許你以多種不同方式定義 URL 路由的可變部分。那麼萬用字元是如何定義的呢?
萬用字元必須是完整的路由段:前面必須有斜線,後面必須有斜線或字串的結尾。
例如,以下三種路由模式都包含有效的萬用字元:
/{message}
/products/{slug}
/{id}/elements
請注意,萬用字元必須是完整的路由段。部分路由段無效:
/product_{id}
/articles/{slug}.html
獲取值
可以使用 *http.Request 型別的 PathValue 方法獲取萬用字元的具體值。
您只需將萬用字元名稱傳給該方法,它就會以字串形式返回其值,如果沒有值,則返回空字串""。
您將在下面的示例中看到 PathValue 的實際應用。
基本示例
下面我們建立一個 /greetings/{greeting} 端點。HTTP 處理程式將獲取萬用字元的值並將其列印到 stdout。
在示例中,我們傳送了 6 個請求,如果一個請求失敗,我們將列印包含網址和狀態程式碼的錯誤資訊。
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// 用 "問候 "萬用字元設定端點。<i> mux.HandleFunc("/greetings/{greeting}", handler)
urls := []string{ "/greetings/hello-world", "/greetings/good-morning", "/greetings/hello-world/extra", "/greetings/", "/greetings", "/messages/hello-world", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func handler(w http.ResponseWriter, r *http.Request) { // 獲取問候語萬用字元的值。<i> g := r.PathValue("greeting") fmt.Printf("Greeting received: %v\n", g) }
|
如果執行該示例,你會發現最後 4 個請求沒有被路由到處理程式。
- /greetings/hello-world/extra 不匹配,因為路由模式中沒有額外的路由段。
- /greetings 和 /greetings/ 因為缺少一個路由段。
- /messages/hello-world,因為第一個路由段不匹配。
多個萬用字元
可以在一個模式中指定多個萬用字元。下面我們在 /chats/{id}/message/{index} 端點中使用了兩個萬用字元。
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// set up the endpoint with a "time" and "greeting" wildcard.<i> mux.HandleFunc("/chats/{id}/message/{index}", handler)
urls := []string{ "/chats/102/message/31", "/chats/103/message/1", "/chats/104/message/4/extra", "/chats/105/", "/chats/105", "/chats/", "/chats", "/messages/hello-world", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func handler(w http.ResponseWriter, r *http.Request) { // get the value for the id and index wildcards.<i> id := r.PathValue("id") index := r.PathValue("index") fmt.Printf("ID and Index received: %v %v\n", id, index) }
|
與前面的示例一樣,每個萬用字元段都必須有一個值。
匹配剩餘部分
模式中的最後一個萬用字元可以選擇匹配所有剩餘路由段,方法是讓其名稱以...結尾。
在下面的示例中,我們使用這種模式將 "步驟 "傳遞給 /tree/ 端點。
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// set up the endpoint with a "steps" wildcard.<i> mux.HandleFunc("/tree/{steps...}", handler)
urls := []string{ "/tree/1", "/tree/1/2", "/tree/1/2/test", "/tree/", "/tree", "/none", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func handler(w http.ResponseWriter, r *http.Request) { // get the value for the steps wildcard.<i> g := r.PathValue("steps") fmt.Printf("Steps received: %v\n", g) }
|
不出所料,前 3 個請求會被路由到處理程式,並將所有剩餘步驟作為值。
與前面的例子不同的是,/tree/ 也與 "步驟 "為空的模式匹配。空餘數 "也算餘數。
/tree 和 /none 仍然不匹配路由模式。
- 但請注意,對 /tree 的請求現在會導致 301 重定向,而不是 404 未找到錯誤。
- 這是因為 http.ServeMux 的尾部斜線重定向行為。這與我們討論的萬用字元無關。
帶尾斜線的模式
如果路由模式以尾部斜線結尾,則會產生 "匿名""... "萬用字元。
這意味著你無法為這個萬用字元取值,但它仍會匹配路由,就像最後一個路由段是 "剩餘匹配段 "一樣。
如果我們將此應用到前面的樹形示例中,就會得到下面的 /tree/ 端點:
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// set up the endpoint with a trailing slash:<i> mux.HandleFunc("/tree/", handler)
urls := []string{ "/tree/1", "/tree/1/2", "/tree/1/2/test", "/tree/", "/tree", "/none", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func handler(w http.ResponseWriter, r *http.Request) { fmt.Printf("URL Path received: %s\n", r.URL.Path) }
|
請注意,我們無法使用 r.PathValue 來檢索步驟,因此我們使用 r.URL.Path 來代替。
不過,匹配規則與之前的示例完全相同。
匹配 URL 結尾
要匹配 URL 的結尾,可以使用特殊萬用字元 {$}。
這在不希望尾部斜線導致匿名"... "萬用字元,而只匹配尾部斜線時非常有用。
例如,如果將我們的樹形端點修改為使用 /tree/{$},那麼現在它將只匹配 /tree/ 請求:
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// set up the endpoint with the match end wildcard:<i> mux.HandleFunc("/tree/{$}", handler)
urls := []string{ "/tree/", "/tree", "/tree/1", "/none", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func handler(w http.ResponseWriter, r *http.Request) { fmt.Printf("URL Path received: %s\n", r.URL.Path) }
|
另一種有用的情況是處理 "主頁 "請求。
/ 模式匹配所有 URL 的請求,但 /{$} 模式只匹配 / 的請求。
設定路由值
在測試或中介軟體中,可能需要在請求中設定路由值。
這可以使用 *http.Request 上的 SetPathValue 方法來實現。該方法接受鍵/值對,隨後對 PathValue 的呼叫將返回該鍵的設定值。
請看下面的示例。
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { req := httptest.NewRequest(http.MethodGet, "/", nil) rr := httptest.NewRecorder()
// 在將請求傳遞給處理程式之前設定路由值。<i> req.SetPathValue("greeting", "hello world")
handler(rr, req) }
func handler(w http.ResponseWriter, r *http.Request) { g := r.PathValue("greeting") fmt.Printf("Received greeting: %v\n", g) }
|
路由匹配和優先順序
一個請求可能會有多個路由匹配。
例如,以下兩條路由:
/products/{id}
/products/my-custom-product
當接收到 URL /products/my-custom-product 的請求時,兩種路由都有可能與之匹配。
那麼實際匹配的是哪一個呢?
在本例中,是最後一個路由,即 /products/我的定製產品。因為它更具體。它比第一條路由匹配的請求更少。
請注意,順序並不重要,即使 /products/{id} 定義在先,也不會被匹配。
下面的示例演示了這一點:
package main
import ( <font>"fmt" "net/http" "net/http/httptest" )
func main() { mux := &http.ServeMux{}
// set up two endpoints<i> mux.HandleFunc("/products/{id}", idHandler) mux.HandleFunc("/products/my-custom-product", customHandler)
urls := []string{ "/products/test", "/products/my-custom-product", }
for _, u := range urls { req := httptest.NewRequest(http.MethodGet, u, nil) rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
resp := rr.Result() if resp.StatusCode != http.StatusOK { fmt.Printf("Request failed: %d %v\n", resp.StatusCode, u) } } }
func idHandler(w http.ResponseWriter, r *http.Request) { fmt.Printf("%s -> idHandler\n", r.URL.Path) }
func customHandler(w http.ResponseWriter, r *http.Request) { fmt.Printf("%s -> customHandler\n", r.URL.Path) }
|
衝突
註冊具有相同特異性且匹配相同請求的路由會導致衝突。在註冊此類路由時,Handler 和 HandleFunc 方法會出現恐慌。
要觸發上例中的panic恐慌,可將 customHandler 的註冊方式從"... "改為"...":
<font>// ...<i> mux.HandleFunc("/products/my-custom-product", customHandler) // ...<i>
|
改為:
mux.HandleFunc(<font>"/products/{name}", customHandler)
|
如果執行程式,就會出現panic恐慌:
panic: pattern "/products/{name}" ... conflicts with pattern "/products/{id}" ...: /products/{name} matches the same requests as /products/{id}
總結
本文討論瞭如何使用 Go 1.22 中引入的萬用字元路由模式實現 URL 路由引數。
主要啟示如下
- 萬用字元可用於在路由中建立一個或多個路由引數。
- 使用 PathValue 方法獲取路由值。
- 使用餘數匹配萬用字元匹配尾部路由段。
- 尾部斜線可作為餘數匹配萬用字元。
- 使用 {$} 萬用字元可禁用此行為。
- 使用 SetPathValue 在請求上設定路由值。
- 路由根據特定性進行匹配。
- 註冊路由可能會引起恐慌。