提升Go的HTTP路由器的提案

译文 精选
开发 前端
这篇文章介绍了一个提升 Go 标准库中的 HTTP 路由器 http.ServeMux 的功能和性能的提案。该提案旨在让 ServeMux 支持匹配 HTTP 方法和通配符路径,从而满足大多数 REST 风格的 API 服务器的需求。文章还对比了其他流行的第三方路由器库,以及讨论了该提案的设计决策和实现细节。

译者 | 刘汪洋

审校 | 重楼

Go 的标准库中包含一个稳定且成熟的 HTTP 服务器。然而,内置的请求路由器http.ServeMux 功能较为简洁,因此你常常需要自己编写路由代码。

其主要短板是,它并未支持 HTTP 方法的匹配(如GET和POST的区别),同时也无法持/users/{user}/settings这种类型的通配符路径。然而,这两个功能几乎是所有 REST 风格的 API 服务器所必需的。

当然,你可以选择自行实现这些功能。在我以前的一篇文章 Go 中不同的 HTTP 路由方法中,提到过有一些优秀的第三方包可以实现更高级的路由功能,并且只需 约 30 行代码 就能够在不借助任何第三方库的情况下实现类似的功能。

但是,未来可能不再需要这些替代方案和第三方包。现在有一个 活跃的提案 - 还包括一个旨在改进 ServeMux 参考实现 ,使其能够匹配 HTTP 方法和通配符路径。

Google 的 Go 团队成员 Jonathan Amsterdam 主导了这个提案以及之前的 讨论。Jonathan 曾成功提出将结构化日志添加到标准库的提案 - Go 1.21 将包含他的log/slog包(预计 2023 年 8 月发布)。

现状与变革

在目前的情况下,如果想将 GET 请求匹配到 /users/{user}/settings,你需要编写以下的样板代码(尽管在实践中你可能会使用第三方库):

mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != "GET" {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    remainder := r.URL.Path[len("/users/"):]
    userId, subPath, _ := strings.Cut(remainder, "/")
    switch subPath {
    case "settings":
        fmt.Fprintf(w, "user %s", userId)
    // 其他子路径可以在这里添加
    default:
        http.NotFound(w, r)
    }
})

如果接受了这个提议,你可以更加简单地实现一样的功能:

mux.HandleFunc("GET /users/{user}/settings", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "user %s", r.PathValue("user"))
})

这样的写法明显更为简洁!

这与其他流行路由器使用的语法非常相似:

// github.com/go-chi/chi
router.Get("/users/{user}/settings", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "user %s", chi.URLParam(r, "slug"))
})

// github.com/gorilla/mux
router.HandleFunc("/users/{user}/settings", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "user %s", mux.Vars(r)["user"])
}).Methods("GET")

// github.com/bmizerany/pat
router.Get("/users/:user/settings", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "user %s", r.URL.Query().Get(":user"))
}))

// github.com/gin-gonic/gin
router.GET("/users/:user/settings", func(c *gin.Context) {
    fmt.Fprintf(w, "user %s", c.Param("user"))
})

提案中的一个有趣决定是,并没有为 ServeMux 添加新的方法;而是对现有的 Handle  HandleFunc 方法进行了扩展,以支持方法前缀和 {wildcard} 路径段。

我理解他们避免添加新方法的想法,但我对这个决定持保留态度。遗憾的是,旧版的 ServeMux 接受如 Handle("GET /foo", h) 的模式。这意味着为增强版 ServeMux 编写的代码将在旧版 Go 上能正常编译和运行,但路由不会匹配到任何内容,这容易导致错误。我可能会添加新的方法,比如 HandleMatch / HandleMatchFunc 或 Route / RouteFunc。

该提议也详细描述了处理两个重叠模式的优先级,其核心规则简单明了:“如果两个模式有重叠(有共同的请求),则更具体的模式优先匹配”。

例如,如果你注册了模式 /users/(匹配 /users/*)以及模式 /users/{user},当一个 /users/ben 的请求进来时,它将匹配第二个,更具体的模式。这与现有的 ServeMux 中,特定主机的模式优先于没有主机名的模式的行为一致。

URL 末尾通配符匹配

此提案为我们带来了一个新的"特殊通配符" {$},它专门用于匹配 URL 的末尾。对于那些仅希望匹配主页路由的情况,这个新特性显得非常实用。在此之前,要实现这一目标颇为麻烦,因为以 / 结尾的模式会匹配所有 / 之下的内容;这个规则对于只有 / 的模式同样适用。

因此,以前若想匹配主页,你需要这样操作:

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" { // 确保路径就是 "/"
        http.NotFound(w, r)
        return
    }
    serveHomepage(w, r)
})
mux.HandleFunc("/users", serveUsers)

这一过程颇为繁琐。若你忘记了路径检查,那么你最终可能会将主页用于所有其他的 URL,而不是显示一个未找到的页面,因为所有的内容都在 / 之下。

而根据新的提案,这个过程将变得更加简洁:

mux.HandleFunc("/{$}", serveHomepage)
mux.HandleFunc("/users", serveUsers)

实现参考

Jonathan 在 github.com/jba/muxpatterns 中发布了一个 ServeMux 的增强版本的示例实现。唯一的区别在于,由于它是在单独的包中,无法改变 http.Request 类型, 所以你需要用 mux.PathValue(request, "name") 来获取路径值,而非 request.PathValue("name")。

我在 我的 go-routing 仓库 中添加了一个 PR,这个 PR 提供了我自己的 widget API 的一种实现,使用 muxpatterns。这个版本与 chi 版本 非常相似 —— 清晰且易读:

r.HandleFunc("GET /{$}", home)
r.HandleFunc("GET /contact", contact)
r.HandleFunc("GET /api/widgets", apiGetWidgets)
r.HandleFunc("POST /api/widgets", apiCreateWidget)
r.HandleFunc("POST /api/widgets/{slug}", apiUpdateWidget)
r.HandleFunc("POST /api/widgets/{slug}/parts", apiCreateWidgetPart)
r.HandleFunc("POST /api/widgets/{slug}/parts/{id}/update", apiUpdateWidgetPart)
r.HandleFunc("POST /api/widgets/{slug}/parts/{id}/delete", apiDeleteWidgetPart)
r.HandleFunc("GET /{slug}", widgetGet)
r.HandleFunc("GET /{slug}/admin", widgetAdmin)
r.HandleFunc("POST /{slug}/image", widgetImage)

当我首次测试这个参考实现时,我发现了一些小问题,现已得到修复。

结论

尽管我对于扩展现有的 Handle 和 HandleFunc 方法有一些保留,但我对这个提案的考虑感到欣慰。鉴于 Jonathan 在提案中的谨慎处理、他在 log/slog 上的良好表现以及社区的积极反馈,此提案被接受的可能性很高。

如果这个功能能进入标准库,那将非常棒 —— 我开发的几乎所有网站和 REST 风格的 API 都将用到这个功能。Go 的标准库已经非常强大,但加入这个功能将进一步减少对第三方路由器的依赖。

如果这个功能能够集成在 2024 年 2 月发布的 Go 1.22 中,我并不会感到惊讶。让我们拭目以待!

译者介绍

刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。

原文标题:The proposal to enhance Go’s HTTP router,作者:Ben Hoyt

责任编辑:华轩 来源: 51CTO
相关推荐

2009-12-22 14:33:51

2010-08-03 13:28:57

2013-10-24 09:43:39

路由器

2011-08-11 13:49:24

2011-08-29 13:04:09

路由器设置路由器连接路由器

2010-08-20 09:16:53

路由器基础

2010-08-16 11:14:25

路由器综合对比

2009-12-30 10:01:00

低端路由器高端路由器

2010-08-19 10:43:40

路由器体系结构

2009-12-22 13:21:59

存储路由器

2017-02-21 09:50:17

2011-02-22 08:57:28

路由器基础

2009-12-11 15:21:15

华为路由器CISCO路由器

2013-07-05 09:28:21

软路由路由技术

2009-11-10 10:10:01

华为路由器

2010-08-19 11:06:19

路由器基础

2010-04-06 17:46:48

2011-09-08 13:05:09

连接无线路由器路由器无线路由器

2011-05-04 15:56:15

路由器网吧

2010-08-05 08:43:40

点赞
收藏

51CTO技术栈公众号