【翻译】golang 1.22 版本 http 路由的路径参数
原文地址: https://www.willem.dev/articles/url-path-parameters-in-routes/
我在处理基于 http 的 API 的时候,通常使用 URL 路径按时(也叫路径变量)来传递数据,这些参数是 URL 的一部分。
几乎所有的 Web 服务器,都会通过路径模式来匹配处理逻辑。 我们希望能在路由中定义参数变量
/products/{slug}
/users/{id}/profile
/{page}
在上面的路由总, {slug} 和 {id} 都是路径参数,{page} 都是命名的路由参数。我们的想法是,我们可以在 http 处理逻辑中获取这些参数。
func handler(w http.ResponseWriter, r *http.Request) {
// Get slug, id or page from in here.
}
在 golang 1.22 版本之前,标准库的 http 路由参数是不支持的, 处理路由参数有些麻烦,或需要使用第三方库。随着 1.22 版本的发布,标准库开始支持 http 路由参数, 我们来按下如何使用这个新特性。
定义路由参数
在 http.ServeMux 类上,我们有2种定义路由的方法,分别是 Handle 和 HandleFunc。它们唯一不同指出在参数,一直接受一个 http.Handler 类型,另一个接受个 http.HandlerFunc 类型的函数。
func(w http.ResponseWriter, r *http.Request)
这篇文章中我们都使用 HandleFunc 方法,因为它更简单。
通配符路由
如果你仔细看这篇文档, 这里并没有提到路径参数,但是它确实支持通配符。
通配符韵如你使用几种方式在 URL 路径中定义变量,例如如下路由参数包含的就是合法的通配符:
/{message}
/products/{slug}
/{id}/elements
或者像下面这样:
/product_{id}
/articles/{slug}.html
获取变量
*http.Request 类型有一个 PathValue 方法能读取到前面定义的路径参数。例如我们创建了一个 /greetings/{greeting} 通配符路由, http Handler 会自动处理这个路由参数,然后会注入到请求对象中
下面这个理由,我们发送6个请求,如果请求失败了,我们就输出所如日志和状态码
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := &http.ServeMux{}
// set up the endpoint with a "greeting" wildcard.
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) {
// get the value for the greeting wildcard.
g := r.PathValue("greeting")
fmt.Printf("Greeting received: %v\n", g)
}
如果你运行上面的代码,你会看到最后的4个请求都没有路由到正确的处理器上。
- /greetings/hello-world/extra 没有匹配到正确的路由,因为它多添加了一段 /extra 路径
- /greetings/ 和 /greetings 不匹配,是一位它们缺少了一段路径参数
- /messages/hello-world 也没有匹配到正确的路由,因为它最前面的第一段参数不匹配 /greetings
多个通配符
可以在一个路由中定义多个路径参数,例如 /chats/{id}/message/{index}
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := &http.ServeMux{}
// set up the endpoint with a "time" and "greeting" wildcard.
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.
id := r.PathValue("id")
index := r.PathValue("index")
fmt.Printf("ID and Index received: %v %v\n", id, index)
}
和前面的例子一样,每一段路径参数都必须要有值,否则路由匹配会失败。
匹配剩余路径
通常在最后一部分的通配符可以匹配剩余所有的路径短,通过在参数名称后面添加…符号。下面这个例子,在 /tree/ 路由创建一个 steps 参数,它匹配所有剩余的路径。
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := &http.ServeMux{}
// set up the endpoint with a "steps" wildcard.
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.
g := r.PathValue("steps")
fmt.Printf("Steps received: %v\n", g)
}
不出所料,前面三个请求都匹配到了正确的处理器上,/tree/ 也能匹配成功,但是 steps 参数的值为空。/tree 就不能匹配成功,因为它缺少了一个 / . /none 也不能匹配成功,因为完全不匹配 /tree 路由。
匹配结尾
如果我们希望能精确匹配 /tree/ 路由,我们可以在参数名称后面添加 {} 符号, 例如: /tree/{$} , 那么它只会匹配 /tree/ 路由。
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := &http.ServeMux{}
// set up the endpoint with the match end wildcard:
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)
}
仅有第一个请求是匹配的,其他都不匹配。
路由匹配优先级
存在定义2个路由,同时间匹配同一个请求的情况,例如:
/products/{id}
/products/my-custom-product
请求 URL /products/my-custom-product 上面2个路由都能匹配,那么会选择哪个路由呢?
实际情况下,后面这个 /products/my-custom-product 会被选择,因为它是更具体的。这里和路由定义的顺序没有关系, 下面这个例子说明了这一点:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := &http.ServeMux{}
// set up two endpoints
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)
}
路由冲突
当注册路由冲突的时候,程序会 panic ,例如:在前面的例子,我们把
// ...
mux.HandleFunc("/products/my-custom-product", customHandler)
// ...
改成
mux.HandleFunc("/products/{name}", customHandler)
那么程序就会 panic ,因为 /products/{id} 和 /products/{name} 路由冲突了。
panic: pattern "/products/{name}" ... conflicts with pattern "/products/{id}" ...: /products/{name} matches the same requests as /products/{id}
总结
本文介绍了 golang 1.22 版本,标准库的 http 路由参数的使用方式,有几个注意实现
- 可以在路由中使用通配符定义路由参数
- 使用 PathValue 获取路由参数的值
- 是有 {setps…} 匹配剩余路径
- 使用 {$}匹配结尾
- 路由注册冲突可能导致程序 panic