从请求开始分析 Go 中间件

117 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情

当用户发起一个接口请求,到达服务的相应的接口处,服务进行请求响应。这一过程中,用户可能想要记录请求日志,限流,权限校验,拦截异常,异常处理等等。

接口请求

首先,从代码入手,我们有一个服务,服务上有三个接口,如下所示:

func main() {
  http.HandleFunc("/hello", helloHandler)
  http.HandleFunc("/list", listHandler)
  http.HandleFunc("/info", infoHandler)
  err := http.ListenAndServe(":8008", nil)
  if err != nil {
    fmt.Print(err)
  }
}
​
func helloHandler(wr http.ResponseWriter, r *http.Request) {
  wr.Write([]byte("hello"))
}
​
func listHandler(wr http.ResponseWriter, r *http.Request) {
  wr.Write([]byte("list"))
}
​
func infoHandler(wr http.ResponseWriter, r *http.Request) {
  wr.Write([]byte("info"))
}

服务正常,一切接口也都可以正常调用输出接口。但是来了一个临时需求:需要统计所有接口的处理耗时

于是,开始吭哧吭哧给每个接口都加上时间的统计,并打印出当前请求所消耗的时间。

func helloHandler(wr http.ResponseWriter, r *http.Request) {
  timeStart := time.Now()
  wr.Write([]byte("hello"))
  timeElapsed := time.Since(timeStart)
  fmt.Println("hello 接口 耗时" + timeElapsed.String())
}
​
func listHandler(wr http.ResponseWriter, r *http.Request) {
  timeStart := time.Now()
  wr.Write([]byte("list"))
  timeElapsed := time.Since(timeStart)
  fmt.Println("list 接口 耗时" + timeElapsed.String())
}
​
func infoHandler(wr http.ResponseWriter, r *http.Request) {
  timeStart := time.Now()
  wr.Write([]byte("info"))
  timeElapsed := time.Since(timeStart)
  fmt.Println("info 接口 耗时" + timeElapsed.String())
}

现有的接口只有几个可能感觉没事,但是当我们接口很多时,每个都这样操作,那将是一场灾难。也许觉得没关系,人力添加无可厚非的事情。当你又接到新的需求时,接口耗时需要上报到其他系统,这样就又需要改动所有的接口。

是否可以将这一部分无关业务的代码和业务代码分开呢?

http.handler

首先了解下 net/http包的 http.handler。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
​
type HandlerFunc func(ResponseWriter, *Request)
​
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
  • Handler 接口只有一个 ServeHTTP(ResponseWriter, *Request) 方法,只要实现了ServerHttp 方法就相当于实现了 http.handler 接口。
  • HandlerFunc 是一个函数类型,实现了 ServeHTTP 方法,即实现了 http.Handler 接口。可以看到 HandlerFunc 函数的 ServerHttp 方法就是调用函数本身 f(w, r).

请求调用链

http.HandleFunc("/hello", helloHandler)
  
func helloHandler(wr http.ResponseWriter, r *http.Request) {
  wr.Write([]byte("hello"))
}

/hello接口为例,helloHandler 实现了 http.handler 接口。当请求过来时就会调用 handler 函数来处理,会调用 HandlerFunc 的 ServeHTTP请求。

h == getHandler() == > h.ServeHTTP() ===> h(w,r)

如果将非业务代码和业务代码分开,那么就需要专门处理时间统计的方法,然后将业务代码作为中间的一个过程。

func reqTimeMiddleware(next http.Handler) http.Handler {
   return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
      timeStart := time.Now()
​
      next.ServeHTTP(wr, r)
      timeElapsed := time.Since(timeStart)
      fmt.Println("接口 耗时" + timeElapsed.String())
   })
}
​
http.Handle("/hello", reqTimeMiddleware(http.HandlerFunc(helloHandler)))
http.Handle("/list", reqTimeMiddleware(http.HandlerFunc(listHandler)))
http.Handle("/info", reqTimeMiddleware(http.HandlerFunc(infoHandler)))

一开始时,使用 http.HandlerFunc(“/hello” ,helloHandler) 是类型转换,把普通函数/方法类型转换成实现了 http.Handler 接口的类型。封装的中间件 reqTimeMiddleware 主要通过包装handler,再返回一个新的handler。

所以可以看到,中间件要做的事情就是通过一个或多个函数对handler进行包装,返回一个包括了各个中间件逻辑的函数链。

优雅中间件使用

在 Gin 框架中,我们可以对中间件有更优雅的写法。

定义一个中间件

func Logger() gin.HandlerFunc {
  return func(c *gin.Context) {
    t := time.Now()
    // 给Context实例设置一个值
    c.Set("geektutu", "1111")
    // 请求前
    c.Next()
    // 请求后
    latency := time.Since(t)
    log.Print(latency)
  }
}

使用中间件

r = NewRouter()
r.Use(logger)

参考资料:www.redhat.com/zh/topics/m…