代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/13-middleware
一、中间件模式常见写法
不管是在http
服务还是rpc
服务中,我们都经常会用到中间件来做一些前置工作,如参数校验、过滤、限流、限频等,而中间件的写法基本是固定的两种,第一种是职责链模式的数组形式,在本人行为型之职责链模式一文中介绍过了,这里要介绍的是第二种常用写法(装饰器模式实现的链表模式)。
首先先定义好中间件的类型,这里我就简单定义为以下的格式,实际工作中基本也是如下格式,只是req
和resp
可能是具体的struct
或者其他类型,或者是拆为多个参数。
/*
ctx: 协程间通信带着
req: 请求的格式,这里图简便,直接interface{}类型
resp: 同req
err: error
*/
type middleware func(ctx context.Context, req interface{}, handler endpoint) (resp interface{}, err error)
handler: endpoint
类型,真正用来处理请求的方法或者是经过N
层中间件包装的后的处理方法
//ctx: 协程间通信带着
//req: 请求的格式,这里图简便,直接interface{}类型
//resp: 同req
//err: error
type endpoint func(ctx context.Context, req interface{}) (resp interface{}, err error)
然后既然我们要将上方的endpoint
进行包装然后产生一个新的endpoint
那么也就是需要一个函数去做这步的事情,输入是endpoint
,输出也是endpoint
。注:middleware
接收endpoint
参数,类似aop
思想对endpoint
做一些增强功能,此外,middleware
接收的参数和返回值,除了参数中有个endpoint
外,其余结构和endpoint
是一样的,这样就能在middleware
中使用endpoint
,将endpoint
需要的参数传给它,并且使得middleware
最终的返回结果也可以和endpoint
一样。
type wrap func(endpoint) endpoint
然后我们通过每次调用这个wrap
的定义去生成一个新的endpoint
,就可以产生一个类似于dfs
链式调用的一个中间件的过程,因为将会一层套一层的endpoint
下去,然后当最后一层有返回了以后就可以接着返回了,然后不断的弹栈回去最开始的地方。
最后,定义一个middlewareChain
,将中间件们串联起来
type middlewareChain []middleware
完整代码如下:注意看注释哦
package main
import (
"context"
"fmt"
)
type endpoint func(ctx context.Context, req interface{}) (resp interface{}, err error)
type wrap func(handler endpoint) endpoint
type middleware func(ctx context.Context, req interface{}, handler endpoint) (resp interface{}, err error)
type middlewareChain []middleware
func main() {
// 1. 各种中间件,如过滤敏感词,限流,限频等,工作中一般每个中间件会分别单独用一个文件写,并用init方式加到链条中
var mdChain middlewareChain
mdChain = append(mdChain, func(ctx context.Context, req interface{}, handler endpoint) (resp interface{}, err error) {
fmt.Println("before 1...")
resp, err = handler(ctx, req)
fmt.Println("after 1...")
return resp, err
})
mdChain = append(mdChain, func(ctx context.Context, req interface{}, handler endpoint) (resp interface{}, err error) {
fmt.Println("before 2...")
resp, err = handler(ctx, req)
fmt.Println("after 2...")
return resp, err
})
// 2. 具体业务handler
handler := func(ctx context.Context, req interface{}) (resp interface{}, err error) {
fmt.Println("具体的业务逻辑,参数为:", req)
return nil, nil
}
// 3. 使用装饰器模式对handler进行层层包装
for i := len(mdChain) - 1; i >= 0; i-- {
// 注:这里的wrap()表示的是类型转换
handler = wrap(func(handler endpoint) endpoint {
// 由于go的机制问题如果不用tmp去存下当前的i,那么mds[i]就会取最终的那一个,就会溢出,
//所以在return前先保存一下i的量,然后每一个stack去存的变量就是对的
curr := i
return func(ctx context.Context, req interface{}) (resp interface{}, err error) {
return mdChain[curr](ctx, req, handler)
}
})(handler) //这里才是wrap(...)(...)表示调用了wrap
}
// 4. 最终的调用
_, _ = handler(context.Background(), "hello middleware")
}
执行结果如下:
二 演进过程
装饰器模式的基本写法
首先我们编写一个简单的 hello
函数:
package main
import "fmt"
func hello() {
fmt.Println("hello middleware!")
}
func main() {
hello()
}
现在我们想在打印 hello middleware!
前后各加一行日志,最直接的实现方式如下:
package main
import "fmt"
func hello() {
fmt.Println("before...")
fmt.Println("hello middleware!")
fmt.Println("after...")
}
func main() {
hello()
}
但更优雅的方式应该是单独编写一个 logger
函数,专门用来打印日志:
package main
import "fmt"
// logger函数参数和返回值都为func(),类似一个装饰器
func logger(f func()) func() {
return func() {
fmt.Println("before...")
f()
fmt.Println("after...")
}
}
func hello() {
fmt.Println("hello middleware!")
}
func main() {
h := logger(hello)
h()
}
logger
函数接收一个函数,并且返回一个函数,而且参数和返回值的函数签名同 hello
函数一样,从而可以将hello
函数传给logger
函数实现包装。输出如下:
Gin中的中间件实现
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.New()
// 使用中间件
r.Use(gin.Logger(), gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
_ = r.Run(":8888")
}
在 Gin 框架中可以通过r.Use(middlewares...)
的方式给路由增加非常多的中间件,这样我们就能够很方便的拦截路由处理函数,并在其前后分别做一些处理逻辑。如上面示例中使用gin.Logger()
增加日志,使用 gin.Recovery()
来处理 panic
异常。
Gin
框架的中间件正是使用装饰模式来实现的,我们可以借用Go
语言自带的http
库来简单模拟下:
package main
import (
"fmt"
"net/http"
)
func loggerMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("before")
f(w, r)
fmt.Println("after")
}
}
func authMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if token := r.Header.Get("token"); token != "mock_token" {
_, _ = w.Write([]byte("unauthorized\n"))
return // 权限校验不通过时直接返回,不用执行后面的f(w,r)了
}
f(w, r)
}
}
func handleHello(w http.ResponseWriter, r *http.Request) {
fmt.Println("handle hello")
_, _ = w.Write([]byte("Hello World!\n"))
}
func main() {
http.HandleFunc("/hello", authMiddleware(loggerMiddleware(handleHello)))
fmt.Println(http.ListenAndServe(":8888", nil))
}
这是一个简单的 Web Server
程序,其监听 8888
端口,当访问 /hello
路由时会进入 handleHello
函数逻辑。
我们分别使用 loggerMiddleware、authMiddleware
函数对 handleHello
进行了包装,使其支持打印访问日志和认证校验功能。假如我们还有其他中间件拦截功能需要加入,就可以这么无限包装下去。
启动这个 Server
来验证下装饰器:
可以对结果进行简单分析,第一次请求/hello
接口时,由于没有携带认证 token
,收到了 unauthorized
响应;第二次请求时携带了 token
,得到响应 Hello World!
,并且后台程序打印如下日志:
说明中间件执行顺序是先由外向内进入,再由内向外返回。而这种一层一层包装处理逻辑的模型有一个非常形象的名字,叫作洋葱模型。
但我们用洋葱模型实现的中间件,相比于 Gin
框架的中间件写法上还差点意思,这种一层层包裹函数的写法不如 Gin
框架提供的 r.Use(middlewares...)
写法直观。
如果你去看 Gin
框架源码,就会发现它的中间件和 handler
处理函数实际上会被一起聚合到路由节点的 handlers
属性中,而这个 handlers
属性其实就是一个 HandlerFunc
类型切片。对应到咱们用 http
标准库实现的 Web Server
中,就是满足 func(ResponseWriter, *Request)
类型的 handler
切片。当路由接口被调用时,Gin
框架就会依次执行 handlers
切片中的所有函数,整个中间件的调用流程就像一条流水线一样依次调用,再依次返回。而这种思想也有一个形象的名字,叫作流水线(Pipeline)
。
接下来我们要做的就是将 handleHello
和两个中间件loggerMiddleware、authMiddleware
聚合到一起,同样形成一个 Pipeline
。
package main
import (
"fmt"
"net/http"
)
func authMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if token := r.Header.Get("token"); token != "fake_token" {
_, _ = w.Write([]byte("unauthorized\n"))
return
}
f(w, r)
}
}
func loggerMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("before")
f(w, r)
fmt.Println("after")
}
}
type handler func(http.HandlerFunc) http.HandlerFunc
// 聚合 handler 和 middleware 类似第一个例子中的wrap,但有一定区别,那里middleware和handle有一定区别
// 这里middleware和handle结构一模一样,所以装饰时,方式有点点不同,但思路基本一致
func pipelineHandlers(h http.HandlerFunc, hs ...handler) http.HandlerFunc {
for i := range hs {
h = hs[i](h)
}
return h
}
func handleHello(w http.ResponseWriter, r *http.Request) {
fmt.Println("handle hello")
_, _ = w.Write([]byte("Hello World!\n"))
}
func main() {
http.HandleFunc("/hello", pipelineHandlers(handleHello, loggerMiddleware, authMiddleware))
fmt.Println(http.ListenAndServe(":8888", nil))
}
我们借用pipelineHandlers
函数将 handler
和 middleware
聚合到一起,实现了让这个简单的Web Server
中间件用法跟 Gin
框架用法相似的效果。