Go 博客

Go 1.22 路由增强

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

Go 1.22 为 net/http 包的路由器带来了两项增强:方法匹配和通配符。这些功能允许您使用模式而不是 Go 代码来表达常用路由。尽管它们易于解释和使用,但在多个模式匹配请求时,找出选择获胜模式的正确规则却是一个挑战。

我们进行这些更改是为了继续努力使 Go 成为构建生产系统的优秀语言。我们研究了许多第三方 Web 框架,提取了我们认为最常用的功能,并将它们集成到 net/http 中。然后,我们通过在 GitHub 讨论提案 issue 中与社区合作,验证了我们的选择并改进了我们的设计。将这些功能添加到标准库意味着许多项目可以减少一个依赖项。但对于现有用户或具有高级路由需求的项目,第三方 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)

此模式匹配一个 GET 请求,其路径以“/posts/”开头,并包含两个段。(特殊情况下,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 请求,而后者匹配任何方法的请求。

“最具体获胜”规则概括了原始模式(没有通配符或 {$})的路径部分的原始“最长获胜”规则。这类模式仅在一个是另一个的前缀时重叠,并且更长的模式更具体。

如果两个模式重叠但 neither is more specific 怎么办?例如,/posts/{id}/{resource}/latest 都匹配 /posts/latest。对于哪一个优先,没有明确的答案,因此我们认为这些模式相互冲突。注册两者(无论顺序如何!)都会导致 panic。

优先级规则对于方法和路径的工作方式与上面完全相同,但为了保持兼容性,我们必须对主机做出一个例外:如果两个模式原本会冲突,并且其中一个有主机而另一个没有,则带有主机的模式优先。

计算机科学专业的学生可能会回想起正则表达式和正则语言的美丽理论。每个正则表达式都会选择一个正则语言,即表达式匹配的字符串集。有些问题通过讨论语言而不是表达式来提出和回答会更容易。我们的优先级规则受到了该理论的启发。事实上,每个路由模式都对应一个正则表达式,而匹配请求的集合扮演着正则语言的角色。

通过语言而不是表达式定义优先级,可以轻松陈述和理解。但是,有一个基于可能无限集合的规则的缺点:不清楚如何高效地实现它。事实证明,我们可以通过逐段遍历来确定两个模式是否冲突。粗略地说,如果一个模式在另一个模式有通配符的地方有一个字面量段,那么它就更具体;但如果字面量在两个方向上都与通配符对齐,则模式会冲突。

当新的模式被注册到 ServeMux 时,它会检查与先前注册模式的冲突。但是检查每对模式将花费二次时间。我们使用索引来跳过不能与新模式冲突的模式;实际上,这效果很好。无论如何,此检查发生在模式注册时,通常在服务器启动时。Go 1.22 中匹配传入请求的时间与之前版本相比变化不大。

兼容性

我们尽了一切努力使新功能与旧版本的 Go 保持兼容。新的模式语法是旧语法的超集,新的优先级规则是对旧规则的泛化。但有一些边缘情况。例如,Go 的先前版本接受带有花括号的模式并将它们视为字面量,但 Go 1.22 使用花括号表示通配符。GODEBUG 设置 httpmuxgo121 可恢复旧行为。

有关这些路由增强的更多详细信息,请参阅 net/http.ServeMux 文档

下一篇文章: 健壮的切片泛型函数
上一篇文章: Go 1.22 发布!
博客索引