Go语言中间件使用方法以及原理解析
什么是中间件?
在网络世界中,中间件针对客户端发起的网络请求而执行的代码,并且将请求链接到下一个中间件,最终到达目标处理函数。当您希望对多个不同请求做一些共享的操作,中间件是很有用的。例如对网络请求进行身份验证、性能日志记录和数据收集。
举一个例子,假如我们有一个服务返回用户提交的消息列表,另一个服务是返回关于该用户的个人敏感信息。只有当用户登录到服务后,才能访问这两个服务。此外,我们还希望记录客户端向服务器发出的每个请求的执行时间。身份验证和日志记录的代码可以与控制器一起编写,并复制到两个服务即可。但是这个例子很容易被分成四个不同的包来管理。一个记录请求数据(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{})