七叶笔记 » golang编程 » 大白话 golang 教程-19-Web 开发和中间件

大白话 golang 教程-19-Web 开发和中间件

回顾一下前面在网络编程中启动 http 服务器的代码,其实只需要一行:

 http.ListenAndServe(":3000", nil)  

访问本机的 3000 端口,你会收到:

 404 page not found  

收到 404 http 的状态码,说明服务器已经正常启动,但是没有任何的路由处理器,所以返回了 404,看看 http.ListenAndServe 函数的签名和实现:

 func ListenAndServe(addr string, handler Handler) error {
  server := &Server{Addr: addr, Handler: handler}
  return server.ListenAndServe()
}  

构建了一个 http.Server 对象,Server 结构第一个是 Addr 对象(string),第二个是 Handler 对象。

 type Server struct {
  Addr string
  Handler Handler // handler to invoke, http.DefaultServeMux if nil
  ......
}  

Addr 是一个字符串表示地址和端口,Handler 是什么呢? 继续 F12 查看:

 type Handler interface { 
  ServeHTTP(ResponseWriter, *Request)
}  

原来是一个实现了 ServeHTTP 函数的接口对象,这个接口恰好就是对 http 请求和响应的封装,而且 Server 的结构有个注释,如果第二个参数是 nil,那么对应的 Handler 对象就是 http.DefaultServeMux,查看 Server 的 ServeHTTP 方法,源代码如下:

 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  handler := sh.srv.Handler
  if handler == nil {
    handler = DefaultServeMux
  }
  if req.RequestURI == "*" && req.Method == "OPTIONS" {
    handler = globalOptionsHandler{}
  }
  handler.ServeHTTP(rw, req)
}  

继续跟踪 DefaultServeMux 发现它是 &defaultServeMux 的引用:

 var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux  

继续跟踪 ServeMux 发现它的 ServeHTTP 方法实现最后被 NotFoundHandler 所处理,难怪是 404:

 func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }
func NotFoundHandler() Handler { return HandlerFunc(NotFound) }  

直接调用 http.HandleFunc 也是作用在这个 defaultServeMux上。 所以要对请求做处理,其实做一个实现了 ServeHTTP 方法的接口对象就行了,它可以安全被转换为 http.Handler 对象,作为一个适配器,http.HandlerFunc 是 http.Handler 的单函数类型适配,也就是说除了让结构体实现 ServeHTTP 方法外,你可以直接提供一个签名为 http.HandlerFunc 的函数作为处理器:

 type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  f(w, r)
}  

小结一下,第一种是提供 http.Handler 对象,然后使用 http.Handler(string,Handler) 来挂载处理器;第二种是提供 http.HandlerFunc(ResponseWriter, *Request) 签名的函数,它们共同的目的都是为了得到一个 ServeHTTP(ResponseWriter, *Request) 函数。

内置的 mux 对路由的支持很简单,在 API 的开发中,很多都遵守 RESTFUL 的设计理念,这时候解析 *Request 的路由就成了很大的体力活儿,需要借助 github.com/julienschmidt/httprouter 模块来帮忙,httprouter 并不是框架,先安装它。

 go get github.com/julienschmidt/httprouter
go: downloading github.com/julienschmidt/httprouter v1.3.0
go: github.com/julienschmidt/httprouter upgrade => v1.3.0  

它的用法很简单:

 router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {})  

用 httprouter 封装后的处理,相比 ServeHTTP 多了 httprouter.Params 对象,它的定义如下:

 type Param struct { 
  Key   string 
  Value string
}

type Params []Param  

定义如下的路由:

 func main() {
  router := httprouter.New()
  router.GET("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintln(w, fmt.Sprintf("%s 你好!", ps.ByName("name")))
  })
  http.ListenAndServe(":3000", router)
}  

其中 :name 成为路径命令参数,访问 显示 “zhangsan 你好!”,查看 GET 的定义,发现 httpRouter 定义了自己的 Handle 处理器:

 type Handle func(http.ResponseWriter, *http.Request, Params)  

Params 的作用就是解析各种路由参数,包括通配符匹配等,其内部使用了压缩字典树结构,这种数据结构的特点是可以高效的做字符串检索,很多 web 框架都使用了 httprouter,比如 ,实现和前面相同功能的例子:

 func startGinServer() {
  router := gin.New() // 干净的没有中间件
  // router := gin.Default() // 会包含 logger 和 recover 中间件
  router.GET("/:name", func(c *gin.Context) {
    c.String(http.StatusOK, "%s 你好!", c.Params.ByName("name"))
  })
  router.Run(":3000")
}  

框架 gin 进一步把 Server 的概念抽象成了 gin.Engine,把请求的上下文抽象成了 gin.Context,提供了输入、输出、插件、验证、安全等很多实用的功能,如果你要做一个正式的 web 项目,可以考虑从 gin 开始。

有了 http.HandlerFunc 和 ServeHTTP 的知识,就可以写 http 中间件了。因为 Handler 的原理是提供函数 func(ResponseWriter, *Request),所以可以提供一个中间件返回这样的函数就行了,其实就是返回 http.Handler,又因为对别的 Handler 的处理其实就是调用 ServeHTTP 方法,所以中间件的函数可以层层包装,在内部实现对 ServeHTTP 的调用,顺便干一点别的事情。简言之,中间件函数的输入是一个 http.Handler,返回也是一个 http.Handler,下面的中间件会在真正要处理的 handler 调用前和完成后打印一个时间点:

 func loggerMiddleware(real http.Handler) http.Handler {
  return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
    fmt.Fprintln(writer, "before "+time.Now().String())
    real.ServeHTTP(writer, request)
    fmt.Fprintln(writer, "\nafter "+time.Now().String())
  })
}  

它的用法如下:

 func startMiddlewareServer() {
  serve1 := func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("我前后有时间"))
  }
  handler := http.HandlerFunc(serve1)
  http.Handle("/1", loggerMiddleware(handler))

  serve2 := func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("我前后也有时间"))
  }
  http.Handle("/2", loggerMiddleware(http.HandlerFunc(serve2)))
  //http.HandleFunc("/2", loggerMiddleware(http.HandlerFunc(serve2)).ServeHTTP)

  http.ListenAndServe(":3000", nil)
}  

访问 输出:

 before 2021-01-12 11:56:11.26169 +0800 CST m=+233.747613357
我前后有时间
after 2021-01-12 11:56:11.261703 +0800 CST m=+233.747626890  

访问 输出:

 before 2021-01-12 11:56:50.768871 +0800 CST m=+273.255964104
我前后也有时间
after 2021-01-12 11:56:50.768895 +0800 CST m=+273.255988769  

中间件运行的顺序是:

 request -> logger-before => real.ServeHTTP => logger-after -> response  

如果再包一层中间件:

 request -> newMiddleware-before => logger-before => real.ServeHTTP => logger-after => newMiddleware-after -> response  

所以中间件是一个洋葱皮的模型,一层一层的,洋葱皮的中心就是被最终包装的 ServeHTTP,外层两边都只是辅助打酱油的,进一步把中间件的类型重新定义一下:

 type middleware func(handler http.Handler) http.Handler
  

中间件就是 middleware 类型的数组,查看 gin.Default() 函数发现它加载中间价的写法是:

 engine := New()
engine.Use(Logger(), Recovery())  

Logger 和 Recovery 函数都返回 HandlerFunc,Use 函数的参数是 …HandlerFunc,它在内部使用 append 把所有的 HandlerFunc 都 append 到列表 Handlers 上,形成了新的 HandlersChain 调用链:

 type HandlersChain []HandlerFunc  

最后通过 gin.Engine 的 addRoute 方法映射到路径上:

 func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { 
  // ...
}  

这个 engine 最后怎么被使用的呢? 它其实就是 http.ListenAndServe 的第二个参数,没错,它本身也作为一个 http.Handler 介入,在 ServeHTTP 里完成分发请求和响应。

 // router.Run(":3000")
func (engine *Engine) Run(addr ...string) (err error) { 
  defer func() { 
    debugPrintError(err) 
  }() 
  address := resolveAddress(addr) 
  debugPrint("Listening and serving HTTP on %s\n", address) 
  err = http.ListenAndServe(address, engine) 
  return
}

// Engine as http.Handler
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { 
  c := engine.pool.Get().(*Context) // 使用了 sync.Pool 减少分配和回收,并发安全
  c.writermem.reset(w) 
  c.Request = req 
  c.reset() 

  engine.handleHTTPRequest(c) // 最后调用点集大成者在此

  engine.pool.Put(c)
}  

到目前为止,应该可以基于 httprouter 写一个能支持 middleware 的 web 简略框架了。

本章节的代码

相关文章