Go 博客

Go 1.22 路由增强

Jonathan Amsterdam 代表 Go 团队
2024 年 2 月 13 日

Go 1.22 为 net/http 包的路由器带来了两项增强功能:方法匹配和通配符。这些功能使您可以将常见的路由表示为模式,而不是 Go 代码。虽然它们解释和使用起来很简单,但想出在多个模式匹配请求时选择获胜模式的正确规则却是一个挑战。

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

增强功能

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

我们将通过一个假设的博客服务器来演示这些更改,其中每篇文章都有一个整数标识符。类似于 GET /posts/234 的请求将检索 ID 为 234 的文章。在 Go 1.22 之前,处理这些请求的代码将以类似于以下的代码行开始

http.HandleFunc("/posts/", handlePost)

尾部斜杠将所有以 /posts/ 开头的请求路由到 handlePost 函数,该函数需要检查 HTTP 方法是否为 GET,提取标识符,并检索文章。由于方法检查对于满足请求来说不是严格必要的,省略它是一个自然错误。这意味着类似于 DELETE /posts/234 的请求将获取文章,这至少令人惊讶。

在 Go 1.22 中,现有代码将继续工作,或者您可以改为编写以下内容

http.HandleFunc("GET /posts/{id}", handlePost2)

此模式匹配路径以“/posts/”开头且有两个段的 GET 请求。(作为特例,GET 也匹配 HEAD;所有其他方法都完全匹配。)handlePost2 函数不再需要检查方法,并且可以使用 Request 上的新 PathValue 方法编写提取标识符字符串的代码

idString := req.PathValue("id")

handlePost2 的其余部分将像 handlePost 一样工作,将字符串标识符转换为整数并获取文章。

如果未注册其他匹配模式,类似于 DELETE /posts/234 的请求将失败。根据 HTTP 语义net/http 服务器将使用 405 Method Not Allowed 错误回复此类请求,该错误将在 Allow 标头中列出可用方法。

通配符可以匹配整个段,例如示例中的 {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/latests 应该优先于 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 文档

下一篇文章:切片的强大通用函数
上一篇文章:Go 1.22 发布!
博客索引