• 首页 > 网络通信频道 > 通信技术

    提升Go的HTTP路由器的提案

    2023年08月18日 16:57:27   来源:51CTO

      译者 | 刘汪洋

      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 工程师,拥有多个主流技术博客平台博客专家称号。

      文章内容仅供阅读,不构成投资建议,请谨慎对待。投资者据此操作,风险自担。

    即时

    TCL实业荣获IFA2024多项大奖,展示全球科技创新力量

    近日,德国柏林国际电子消费品展览会(IFA2024)隆重举办。凭借在核心技术、产品设计及应用方面的创新变革,全球领先的智能终端企业TCL实业成功斩获两项“IFA全球产品设计创新大奖”金奖,有力证明了其在全球市场的强大影响力。

    新闻

    敢闯技术无人区 TCL实业斩获多项AWE 2024艾普兰奖

    近日,中国家电及消费电子博览会(AWE 2024)隆重开幕。全球领先的智能终端企业TCL实业携多款创新技术和新品亮相,以敢为精神勇闯技术无人区,斩获四项AWE 2024艾普兰大奖。

    企业IT

    重庆创新公积金应用,“区块链+政务服务”显成效

    “以前都要去窗口办,一套流程下来都要半个月了,现在方便多了!”打开“重庆公积金”微信小程序,按照提示流程提交相关材料,仅几秒钟,重庆市民曾某的账户就打进了21600元。

    3C消费

    “纯臻4K 视界焕新”——爱普生4K 3LCD 激光工程投影

    2024年3月12日,由爱普生举办的主题为“纯臻4K 视界焕新”新品发布会在上海盛大举行。

    研究

    2024全球开发者先锋大会即将开幕

    由世界人工智能大会组委会、上海市经信委、徐汇区政府、临港新片区管委会共同指导,由上海市人工智能行业协会联合上海人工智能实验室、上海临港经济发展(集团)有限公司、开放原子开源基金会主办的“2024全球开发者先锋大会”,将于2024年3月23日至24日举办。