什么是中间件?

如果你已经熟悉中间件,只是想了解如何实践,可以跳过这部分。

在网络世界中,中间件针对客户端发起的网络请求而执行的代码,并且将请求链接到下一个中间件,最终到达目标处理函数。当您希望对多个不同请求做一些共享的操作,中间件是很有用的。例如对网络请求进行身份验证、性能日志记录和数据收集。

举一个例子,假如我们有一个服务返回用户提交的消息列表,另一个服务是返回关于该用户的个人敏感信息。
只有当用户登录到服务后,才能访问这两个服务。此外,我们还希望记录客户端向服务器发出的每个请求的执行时间。身份验证和日志记录的代码可以与控制器一起编写,并复制到两个服务即可。但是这个例子很容易被分成四个不同的包来管理。一个记录请求数据(LOG),一个验证用户是否登录(AUTH),一个返回消息列表(RETURN MSG),和一个返回用户数据(RETURN USER)。

通过将这些功能作为中间件来编写,我们可以很容易将请求连接起来,并在需要时在每个中间件上提前返回。例如,用户没有登录就发送请求。
整个处理过程可能是如下方式:

[LOG] -> [AUTH] -> [RETURN MSG]
[LOG] -> [AUTH] -> [RETURN USER]

由于分离了LOG和AUTH,我们只需要编写代码和测试一次就可以。

Go中间件

在Go中,所有的网络请求都通过实现net/http包中Handler接口来完成。对不太熟悉Go的开发人员来说,Handler是响应请求并处理对请求参数的读取和响应的写入。因为Go在其关键函数中都需要这个接口,所以围绕它构建包或者装饰器来扩展接口是很容易并且可靠的。

创建中间件的快速方法就是封装net/http.Handler.ServeHTTP方法:

ServeHTTP(ResponseWriter, *Request)

通过添加一个外部函数,该函数接收一个Handler类型参数,并使用HandlerFunc函数来返回一个Handler:

func middleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Code for the middleware...
    next.ServeHTTP(w, r)
  })
}

上面的函数接收一个Handler类型的参数,该参数将请求链接到最终的函数上去。因为返回值也是Handler类型,可以将调用串起来:

firstMiddleware(secondMiddleware(lastHandler))

仅通过这个简单的方法,就可以很好地实现中间件。虽然不是很健壮,但是可行的。

接下来,我将使用Negroni这个包,使创建中间件变得更简单。当然你也可以自己随意的实现。Negroni封装了router,并为中间件的管理提供了功能。比如更简单,灵活地连接中间件。

实现-对Handler的封装

为了尽可能的简单,下面实现一个中间件来记录网络请求的执行时间。将使用Negroni来实现,先创建router然后用Negroni来封装:

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/urfave/negroni"
)

func main() {
    router := mux.NewRouter()
    router.
        Methods("GET").
        Path("/").
        HandlerFunc(endpointHandler)

    n := negroni.New()
    n.UseHandler(router)

    err := http.ListenAndServe(":8080", n)
    if err != nil {
        panic(err)
    }
}

func endpointHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Endpoint handler called")
}

如上面代码所示,先用gorilla/mux包创建router,然后注册一个Get服务。并且创建一个Negroni实例,将router作为参数传入UserHanler函数。因为Negroni实现了Handler接口,可以像使用router那样来使用它。运行以上代码并发起Get请求(curl localhost:8080/ ),可以观察到“Endpoint handler called”内容返回给客户端。

中间件的实现

下面继续创建中间件,如前所述,中间件就是一个Handler。因此,我们需要做的就是创建一个结构体来实现Handler接口。这里我们使用Negroni,我们只需要实现它的Handler接口即可。

如下所示,和net/http.Hanler接口类似,除了它还支持与下一个参数链接:

type Handler interface {
    ServeHTTP(
        rw http.ResponseWriter, 
        r *http.Request, 
        next http.HandlerFunc,
)
}

让我们创建一个简单的打印日志程序:

package mw

import (
    "fmt"
    "net/http"
    "time"
)

type Logger struct{}

func (*Logger) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    fmt.Println("The logger middleware is executing!")
    t := time.Now()
    next.ServeHTTP(w, r)
    fmt.Printf("Execution time: %s \n", time.Now().Sub(t).String())
}

上面的代码展示了如何通过实现Negroni.Handler来创建一个简单的中间件。对请求的执行时间进行记录。最后,继续执行下一个Handler,它可能是另一个中间件或是最终的处理函数。在该调用完成执行后,记录总的执行时间。

将上面日志中间件添加到router上,看看执行时间:

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/johan-lejdung/go-microservice-middleware-guide/mw"
    "github.com/urfave/negroni"
)

func main() {
    router := mux.NewRouter()
    router.
        Methods("GET").
        Path("/").
        HandlerFunc(endpointHandler)

    n := negroni.New()
    n.Use(&mw.Logger{})
    n.UseHandler(router)

    err := http.ListenAndServe(":8080", n)
    if err != nil {
        panic(err)
    }
}

func endpointHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Endpoint handler called")
}

我们只是使用n.Use(&mw.Logger{}),告诉Negroni调用创建的Logger中间件,中间件会按照添加顺序执行。执行上面的代码就会得到如下日志:

The logger middleware is executing!
Endpoint handler called
Execution time: 25.146µs

第一行和第三行是从中间件生成的,而第二行是在函数endpoint Handler中生成的。我们成功地实现了中间件。

让我们考虑一下,我们还有一个身份验证中间件(AuthMW),这个AuthMW将确保未经授权的请求无法到达最终执行函数。要将它添加到链中,我们只需执行以下操作:

n.Use(&mw.Logger{})
n.Use(&mw.Auth{})

我们已经创建了中间件链的第一部分。上面的代码将在继续访问AUTH中间件之前执行Logger。以同样的方式,你可以为单个路由器路径创建一个中间件链,如果我们将这两种连接中间件的方法结合起来,我们便能够获得一些非常强大的工具去创建我们的路由和我们想要它们执行的代码!

完整代码地址 https://github.com/johan-lejdung/go-microservice-middleware-guide

转自:jianshu.com/p/ec9a67e7f7a7