探索Golang 1.22中优化的HTTP服务器路由

发表时间: 2023-10-17 08:30

对于Web开发的人来说,使用golang语言无疑是个明智的选择。但是golang本身标准库提供的功能相对有限,尤其是需要撰写复杂的路由和路劲匹配(这在微服务中很常见)时的多路复用功能,必须要配合使用第三方的多路复用器(比如gorilla/mux)。



但是最新一个好消息是,golang 1.22 中的标准net/http将默认提供增强模式匹配能力包多路复用器。

本文我们就一起来学习一下这个即将到来的新的功能。

概述

现有的多路复用器(http.ServeMux)只能提供基本的路径匹配,很多时候要借助于第三方的库来实际中需求的功能。

在最新的一个提案中,我们了解到了Golang 1.22将会增强http.ServeMux的多路匹配功能,包括通配符、优先级等。

新的多路复用器

如果曾经使用过第3方多路复用器/路由器包(例如gorilla/mux)的同学,则新的http.ServeMux的多路复用器用法也比较类似,一个简单例子:

package mainimport ("fmt""net/http")func main() {mux := http.NewServeMux()mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprint(w, "got path\n")})mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {id := r.PathValue("id")fmt.Fprintf(w, "handling task with id=%v\n", id)})http.ListenAndServe("localhost:8090", mux)}

以上代码中,我们可以看出:

HTTP方法(本例中为GET在第一个处理程序中)明确地作为模式的一部分。这意味着该处理程序只会触发对以/path/开头的路径的GET请求,而不是其他HTTP方法。

在第二个处理程序中,第二个路径组件中有一个通配符{id},这是新增加的功能。通配符将匹配请求的单个路径组件和处理程序可以通过PathValue方法访问匹配的值

Go 1.22尚未发布,在试验环境中建议使用gotip命令运行示例:

gotip run sample.go


在一个单独的终端中,然后,可以通过curl命令来模拟请求测试:

curl localhost:8090/what/404 page not foundcurl localhost:8090/path/got pathcurl -X POST localhost:8090/path/Method Not Allowedcurl localhost:8090/task/f0r32e/handling task with id=f0r32e​

注意服务器拒绝POST对/path/的请求,而只允许GET请求(curl默认)。另请注意id通配符是如何获取的当请求匹配时分配一个值。

模式匹配

模式可以包含{name}或者{name...}形式的通配符路径元素。例如,/b/{bucket}/o/{objectname...}。

名称必须是有效的Go标识符;也就是说,它必须完全匹配正则表达式 [\pL][_\pL\p{Nd}]*。

通配符必须是完整路径元素,前面必须有斜杠,后面必须有斜杠或字符串末尾。 例如,/b_{bucket}不是有效的模式。此类情况可以通过处理程序本身的附加逻辑来解决。一个正确写法为/{bucketlink}并从值中解析实际的存储桶名称 bucketlink。或者,使用其他路由器。

通常,通配符仅匹配单个路径元素,以请求URL中的下一个斜杠(不是%2F)结束。如果...存在,则通配符与URL路径的其余部分匹配,包括斜杠。(因此对于...通配符出现在模式末尾以外的任何位置)尽管通配符匹配发生在转义路径上,但通配符值不会转义。例如,如果通配符匹配a%2Fb,其值为a/b。

最后还有一个特殊的通配符:{$}仅匹配URL的结尾,允许编写以斜杠结尾的模式,但不匹配该路径的所有扩展名。例如,模式/{$}匹配根页面/但是不匹配/anythingelse。

优先级

有一个优先规则:如果两个模式重叠(有一些共同的请求),则更具体的模式优先。如果P1与P2请求的(严格)子集匹配,则模式P1比P2更具体;也就是说,如果P2匹配P1及更多的所有请求。如果两者都不是更具体,那么模式就会发生冲突。

为了向后兼容,该规则有一个例外:如果两个模式会发生冲突,并且一个具有主机而另一个没有,则具有主机的模式优先。

这些维恩图说明了两个模式 P1 和 P2 之间根据它们匹配的请求之间的关系:

以下是一些示例,其中一种模式比另一种模式更具体:

example.com/ 比 / 更具体因为第一个仅匹配主机的请求example.com,而第二个匹配任何请求。

GET / 比/更具体因为第一个仅匹配GET和HEAD请求,而第二个匹配任何请求。

HEAD / 比GET /更具体因为第一个仅匹配HEAD请求,而第二个匹配GET和HEAD 请求。

/b/{bucket}/o/default 比/b/{bucket}/o/{noun}更具体因为第一个仅匹配第四个元素是文字“default”的路径,而在第二个中,第四个元素可以是任何内容。

与上一个示例相反,模式/b/{bucket}/{verb}/default和 /b/{bucket}/o/{noun}相互冲突:

它们重叠,因为两者都匹配路径/b/k/o/default。且:

第一个不是更具体,因为它与路径匹配 /b/k/a/default而第二个则不然。

第二个不是更具体,因为它与路径匹配 /b/k/o/n而第一个没有。

使用特异性进行匹配很容易描述,并且保留了原始 ServeMux模式的顺序无关性。 但很难一眼看出两种模式中哪一种更具体,或者为什么两种模式会发生冲突。 因此,注册冲突模式时生成的紧急消息将通过提供示例路径来演示冲突,如上一段所示。

特别的看如下示例:

mux := http.NewServeMux()mux.HandleFunc("/task/{id}/status/", func(w http.ResponseWriter, r *http.Request) {id := r.PathValue("id")fmt.Fprintf(w, "handling task status with id=%v\n", id)})mux.HandleFunc("/task/0/{action}/", func(w http.ResponseWriter, r *http.Request) {action := r.PathValue("action")fmt.Fprintf(w, "handling task 0 with action=%v\n", action)})

请求假设服务器收到对/task/0/status/——其中处理程序应该去吗?两者看似都很匹配。实际上发生了冲突,注册会触发panic。事实上,对于上面的例子中,会有错误panic告警:

panic: pattern "/task/0/{action}/" (registered at sample-conflict.go:14) conflicts with pattern "/task/{id}/status/" (registered at sample-conflict.go:10):/task/0/{action}/ and /task/{id}/status/ both match some paths, like "/task/0/status/".But neither is more specific than the other./task/0/{action}/ matches "/task/0/action/", but /task/{id}/status/ doesn't./task/{id}/status/ matches "/task/id/status/", but /task/0/{action}/ doesn't.

该错误消息详细且有帮助。如果遇到复杂的冲突注册方案(特别是当模式在多个地方注册时在源代码中),该消息能帮助我们迅速定位到问题所在,并进行修复。

总结

“应该使用哪个路由器包?” 一直是Go web开发初学者的常常疑惑地问题。相信随着Golang 1.22的发布,这个问题将不再是个问题。新的stdlib mux功能将会满足绝大多数人的需要。当然你也可以继续使用喜欢的第三方包,这也一点都不会冲突,只是给我们一个新的,更便捷选择而已。