..

【翻译】golang 1.22 版本 http 路由的路径参数

原文地址: https://www.willem.dev/articles/url-path-parameters-in-routes/

我在处理基于 http 的 API 的时候,通常使用 URL 路径按时(也叫路径变量)来传递数据,这些参数是 URL 的一部分。

几乎所有的 Web 服务器,都会通过路径模式来匹配处理逻辑。 route 我们希望能在路由中定义参数变量

/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