原文在这里[1]。
由 Jonathan Amsterdam代表Go团队发布于2024年2月13日
Go 1.22对net/http
包的路由进行了两项增强:方法匹配和通配符。这些功能允许你将常见的路由表示为模式,而不是Go代码。尽管它们很容易解释和使用,但在选择多个匹配请求的模式时,确定胜出的模式的规则是一个挑战。
我们进行这些更改是为了继续努力使Go成为构建生产系统的优秀语言。我们研究了许多第三方Web框架,提取了我们认为是最常用的功能,并将其集成到net/http
中。然后,通过在GitHub 讨论[2]和提案问题[3]中与社区合作,验证了我们的选择并改进了我们的设计。将这些功能添加到标准库意味着对许多项目来说,少了一个依赖项。但对于当前用户或具有高级路由需求的程序,第三方Web框架仍然是一个不错的选择。
提升
新的路由功能几乎只影响传递给两个net/http.ServeMux
方法Handle
和HandleFunc
的模式字符串,以及相应的顶级函数http.Handle
和http.HandleFunc
。唯一的API更改是net/http.Request
上的两个用于处理通配符匹配的新方法。
我们将通过一个虚构的博客服务器示例来说明这些更改,在该服务器中每篇帖子都有一个整数标识符。像GET /posts/234
这样的请求会检索ID
为234
的帖子。在Go 1.22之前,处理这些请求的代码可能会以以下方式开始:
http.Handle("/posts/", handlePost)
具有尾随斜杠的模式将所有以/posts/
开头的请求路由到handlePost
函数,该函数必须检查HTTP方法是否为GET,提取标识符并检索帖子。由于方法检查并不是满足请求的严格必要条件,忽略它是一个显而易见的错误。这将意味着像DELETE /posts/234
这样的请求将获取帖子,这至少是令人惊讶的。
在Go 1.22中,现有的代码将继续工作,或者您可以改为编写:
代码语言:javascript复制http.Handle("GET /posts/{id}", handlePost2)
这个模式匹配以/posts/
开头且有两个路径段的GET请求(作为特例,GET还匹配HEAD;所有其他方法完全匹配)。handlePost2
函数不再需要检查方法,提取标识符字符串可以使用Request
上的新PathValue
方法编写:
idString := req.PathValue("id")
handlePost2
的其余部分的行为与handlePost
相似,将字符串标识符转换为整数并获取帖子。
对于类似DELETE /posts/234
的请求,如果没有注册其他匹配的模式,它们将失败。根据HTTP 语义[4],net/http
服务器将用一个包含可用方法的Allow标头回复此类请求的405 Method Not Allowed错误。
通配符可以匹配整个路径段,如上面的示例中的{id}
,或者如果以...
结尾,它可以匹配路径的所有剩余段,如模式/files/{pathname...}
。
还有最后一点语法。如上所示,以斜杠结尾的模式,如/posts/
,将匹配以该字符串开头的所有路径。要仅匹配具有尾随斜杠的路径,可以写为/posts/{$}
。这将匹配/posts/
,但不匹配/posts
或/posts/234
。
最后还有一点API:net/http.Request
具有SetPathValue
方法,以便标准库之外的路由器可以通过Request.PathValue
公开它们自己路径解析的结果。
优先级
每个HTTP路由器都必须处理重叠的模式,比如/posts/{id}
和/posts/latest
。这两个模式都匹配路径posts/latest
,但最多只能有一个用于处理请求。哪个模式具有优先权?
有些路由器不允许重叠,也有其它的使用最后注册的模式。Go一直允许重叠,并且选择较长的模式,而不考虑注册顺序。保持顺序独立性对我们来说很重要(并且对向后兼容性是必需的),但我们需要比"最长赢"更好的规则。该规则会选择/posts/latest
而不是/posts/{id}
,但会选择/posts/{identifier}
而不是两者。这似乎是错误的:通配符名称不应该影响结果。感觉像是/posts/latest
应该始终在这场比赛中获胜,因为它匹配单个路径而不是多个路径。
我们追求一个好的优先规则,考虑了许多模式的属性。例如,我们考虑首选具有最长字面(非通配符)前缀的模式。这会选择/posts/latest
而不是/posts/{id}
。但它不能区分/users/{u}/posts/latest
和/users/{u}/posts/{id}
,而且似乎前者应该优先。
我们最终选择了一个基于模式含义而不是外观的规则。每个有效的模式都匹配一组请求。例如,/posts/latest
匹配路径/posts/latest
的请求,而/posts/{id}
匹配具有任何第一段是posts
的两段路径的请求。我们说,如果一个模式匹配的请求严格子集属于另一个模式匹配的请求,则该模式比另一个更具体。模式/posts/latest
比/posts/{id}
更具体,因为后者匹配前者匹配的每个请求,以及更多。
优先规则很简单:最具体的模式获胜。这个规则符合我们的直觉,即posts/latest
应该优先于posts/{id}
,而/users/{u}/posts/latest
应该优先于/users/{u}/posts/{id}
。对于方法来说也是有道理的。例如,GET /posts/{id}
优先于/posts/{id}
,因为前者仅匹配GET
和HEAD
请求,而后者匹配任何方法的请求。
“最具体者获胜”规则概括了最初的“最长者获胜”规则,用于原始模式的路径部分,即没有通配符或{$}
的部分。这样的模式只有在一个是另一个的前缀时才会重叠,而较长者更具体。
如果两个模式重叠但没有一个更具体,怎么办?例如,/posts/{id}
和/{resource}/latest
都匹配/posts/latest
。对于这两者哪个更具优势并没有明显的答案,所以我们认为这些模式彼此冲突。注册这两者中的任何一个(无论顺序如何!)都会导致 panic。
优先级规则在方法和路径方面完全按照上述方式工作,但为了保持兼容性,我们必须为主机破例一次:如果两个模式在其他方面会发生冲突,且其中一个有主机而另一个没有,那么带有主机的模式优先。
计算机科学的学生可能会记得正则表达式和正则语言的美丽理论。每个正则表达式都选择一个正则语言,即由该表达式匹配的字符串集。通过讨论语言而不是表达式,有些问题更容易提出和回答。我们的优先规则受到了这个理论的启发。实际上,每个路由模式对应一个正则表达式,而匹配请求的集合则充当正则语言的角色。
通过语言而不是表达式定义优先级易于陈述和理解。但基于潜在无限集合的规则也有一个缺点:如何高效实现它并不明确。事实证明,我们可以通过逐段遍历模式来确定两个模式是否冲突。粗略地讲,如果一个模式在另一个模式有通配符的地方有一个字面段,那么它更具体;但如果字面值与两个方向的通配符对齐,则这两个模式冲突。
当在ServeMux
上注册新模式时,它会检查与先前注册的模式是否存在冲突。但是检查时需要耗费额外的时间,所以我们使用索引跳过不可能与新模式冲突的模式。在实践中,它的工作效果相当好。无论如何,此检查发生在模式注册时,通常是在服务器启动时。在Go 1.22中,匹配传入请求的时间与以前的版本相比并没有太大变化。
兼容性
我们尽一切努力确保新功能与较早版本的Go兼容。新的模式语法是旧语法的超集,新的优先规则是旧规则的泛化。但也有一些边缘情况。例如,之前的Go版本接受具有大括号的模式并将其视为字面量,但Go 1.22使用大括号作为通配符。GODEBUG
设置httpmuxgo121
可以恢复旧的行为。
有关这些路由增强功能的更多详细信息,请参阅net/http.ServeMux 文档[5]。
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)[6]进行许可,使用时请注明出处。 Author: mengbin[7] blog: mengbin[8] Github: mengbin92[9] cnblogs: 恋水无意[10] 腾讯云开发者社区:孟斯特[11]