Go 1.22中的路由提升

2024-02-21 17:12:00 浏览数 (1)

原文在这里[1]。

由 Jonathan Amsterdam代表Go团队发布于2024年2月13日

Go 1.22对net/http包的路由进行了两项增强:方法匹配通配符。这些功能允许你将常见的路由表示为模式,而不是Go代码。尽管它们很容易解释和使用,但在选择多个匹配请求的模式时,确定胜出的模式的规则是一个挑战。

我们进行这些更改是为了继续努力使Go成为构建生产系统的优秀语言。我们研究了许多第三方Web框架,提取了我们认为是最常用的功能,并将其集成到net/http中。然后,通过在GitHub 讨论[2]和提案问题[3]中与社区合作,验证了我们的选择并改进了我们的设计。将这些功能添加到标准库意味着对许多项目来说,少了一个依赖项。但对于当前用户或具有高级路由需求的程序,第三方Web框架仍然是一个不错的选择。

提升

新的路由功能几乎只影响传递给两个net/http.ServeMux方法HandleHandleFunc的模式字符串,以及相应的顶级函数http.Handlehttp.HandleFunc。唯一的API更改是net/http.Request上的两个用于处理通配符匹配的新方法。

我们将通过一个虚构的博客服务器示例来说明这些更改,在该服务器中每篇帖子都有一个整数标识符。像GET /posts/234这样的请求会检索ID234的帖子。在Go 1.22之前,处理这些请求的代码可能会以以下方式开始:

代码语言:javascript复制
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方法编写:

代码语言:javascript复制
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},因为前者仅匹配GETHEAD请求,而后者匹配任何方法的请求。

“最具体者获胜”规则概括了最初的“最长者获胜”规则,用于原始模式的路径部分,即没有通配符或{$}的部分。这样的模式只有在一个是另一个的前缀时才会重叠,而较长者更具体。

如果两个模式重叠但没有一个更具体,怎么办?例如,/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]

0 人点赞