跳过正文
目录
  1. 文章/

从头构建 Go Web 框架(四):第三方路由集成

·1887 字·4 分钟
POLO XUE
作者
POLO XUE
技术探索与学习记录

本系列文章写于 2014 年,相较于 golang 极短的发展历程,这已经是古董级别的一篇文章了,但 web 框架思想概念依然有效。希望通过翻译这个系列文章,能让大家都现有 Go Web 框架有更深的认识。

本文是 “构建属于自己的 Web 框架” 系列文章中的第四篇,将介绍如何在 Go 中使用三方路由。

基于 Go 标准库 net/http,已经足够写出一个 Web 应用。但不足的是,它提供的路由能力 http.Handle(pattern, handler) 还是过于单一,只能实现一些静态路由。

这就是为什么我们需要一个优秀的三方路由。

然如此多的第三方路由,都有各自的特点,究竟该如何选择?

当接触一门新的编程语言,如果有 10 个不同库实现相同能力,将很难了解什么是最佳实践。我们希望有一种速度快、内存高效且易于使用的 router。

如下是我认为 Go 中最常用的 router,将从执行速度、内存消耗等维度对比。

gorilla/mux
#

gorilla/mux 是一款成熟的 router,同时也是 Go 中最流行的三方路由。它有着丰富的功能,缺点是速度慢且内存消耗验证。

且,gorilla/mux 支持正则 URL 参数约束,如下所示:

r := mux.NewRouter()
r.HandleFunc("/teas/{category}/", TeasCategoryHandler)
r.HandleFunc("/teas/{category}/{id:[0-9]+}", TeaHandler)

HTTP 方法配置路由,如下:

r.Methods("GET", "HEAD").HandleFunc("/teas/{category}/", TeasCategoryHandler)

和其他路由的不同,gorilla/mux 有丰富的内置匹配规则,支持如 host(如子域名)、前缀、协议(http、https 等)、HTTP 头、查询参数。如果这些还不能满足你,通过自定义方式,如下方式:

// Proto      string // "HTTP/1.0"
// ProtoMajor int    // 1
// ProtoMinor int    // 0
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
  return r.ProtoMajor == 0
})

在 Handler 函数中,通过 mux.Vars(request) 可获取 URL 参数,它和上文介绍的 gorilla/context 类似。

代码如下:

func myHandler(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  category := vars["category"]
}

这个方案的优势是,它与 http.Handler 接口兼容。这点其实非常重要,因为我们的应用越多,共享 handler 和 middleware 的可能越大,就更加需要遵循一定的规则。

优势:功能强大,轻松创建复杂的路由规则,且与 http.Handler 兼容。 劣势:速度慢且内存消耗严重,如果看中速度的话,它不适合你。

httprouter
#

httprouter, 号称 “最快的 router”。httprouter 的作者对不同的 router 做了基准测试,具体查看 go-http-routing-benchmark

httprouter 比 gorilla/mux 简单,但它不支持约束和正则,对于 REST API 而言,这个缺点的影响不大,但如果希望创建复杂的路由,这个简化设计就会大大限制它的适用范围。

还有,它与 http.Handler 不兼容,它定义了一个新的 interface,拥有三个参数,其中第三个参数用于访问 URL 参数。

示例代码:

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)
}

但这个问题也容易解决,将 URL 参数注入 context 中,实现在标准 interface http.Handler 和 httprouter 接口间的转换。这种方式会损失一些性能,但依然是一个 faster router。

如何实现?后续具体实现时介绍。

优点:快。

缺点:与 http.Handler 不兼容。

Pat
#

Pat 也是一个流行且简单的 router。它与 http.Handler 完全兼容。但它不是用 context 存储 URL 参数,而是将参数保存在 request 中,通过 r.URL.Query() 获取。

示例代码:

func Hello(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "hello, %s!\n", r.URL.Query().Get(":name"))
}

func main() {
  m := pat.New()
  m.Get("/hello/:name", http.HandlerFunc(Hello))
}

缺点是 r.URL.Query() 每次都是从原始 querystring 中解析参数,这是对性能不友好的行为,如果包含经过多个中间件,这对性能的影响将更大。速度方面,pat 相较于 httprouter 要慢十倍。

优势: 与 http.Handle 兼容.

劣势: 有点慢.

如何选择?
#

如果是传统 Web 应用,服务端进行页面渲染,因为需要复杂的路由,gorilla/mux 是最好的选择。如果是 REST API,httprouter 更加适用,因而,我们将基于 httprouter 完善我们的程序。

集成 httprouter
#

由于 httprouter 与 http.Handler 不兼容,要进行一些调整。实现方案,将中间件栈(http.Handler)包裹,从而实现 httprouter.Handler 接口。

代码如下:

func wrapHandler(h http.Handler) httprouter.Handle {
  return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    context.Set(r, "params", ps)
    h.ServeHTTP(w, r)
  }
}

func main() {
  db := sql.Open("postgres", "...")
  appC := appContext{db}
  commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
  router := httprouter.New()
  router.GET("/admin", wrapHandler(commonHandlers.Append(appC.authHandler).ThenFunc(appC.adminHandler)))
  router.GET("/about", wrapHandler(commonHandlers.ThenFunc(aboutHandler)))
  router.GET("/", wrapHandler(commonHandlers.ThenFunc(indexHandler)))
  http.ListenAndServe(":8080", router)
}

通过 wrapHandler 实现将中间件 http.Handlerhttprouter.Hande 间的转化,从而实现拥有 httprouter 的良好性能的同时,也能与http.Handler的兼容。

接下来,演示如何在 handler 使用 URL 参数。

创建路由:

router.GET("/teas/:id", wrapHandler(commonHandlers.ThenFunc(appC.teaHandler)))

如下代码,创建 teaHandler,其中将通过 id 从数据库中查询数据。

func (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) {
  params := context.Get(r, "params").(httprouter.Params)
  tea := getTea(c.db, params.ByName("id"))
  json.NewEncoder(w).Encode(tea)
}

总结
#

Go 中的不同 router 的性能差异很大,功能也有差异。最快的路由器并不一定适合你的项目。httprouter 非常适合于 REST API 这样的简单路由,gorilla/mux 更适合具传统的 web 应用。

对于不兼容与 http.Handler 的路由实现,可通过类似 wrapHandler 实现兼容。

最后,不同 router 方案存储 URL 参数的方式不同,常见的两种方式: r.URL.Query() 和 context。在实际使用时,要注意规范一致。

我的博文:从头构建 Go Web 框架(四):第三方路由集成 ,原文地址: Part 4: Guide to 3rd Party Routers in Go