• 如何解决Go内置的请求路由器http.ServeMux功能较为单一的问题?
  • 发布于 2个月前
  • 194 热度
    0 评论
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)
    // 堆代码 duidaima.com
    // 其他子路径可以在这里添加
    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"))
})
// 堆代码 duidaima.com
// 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 中,我并不会感到惊讶。让我们拭目以待!
用户评论