Go 1.22中路由 URL 路由引數

banq發表於2024-02-16

處理基於 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 在請求上設定路由值。
  • 路由根據特定性進行匹配。
  • 註冊路由可能會引起恐慌。

相關文章