代码地址: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 框架用法相似的效果。
本文介绍了在Golang中使用装饰器模式实现的中间件,包括责任链模式数组形式和装饰器链式调用,展示了如何在HTTP服务和RPC中应用,以及Gin框架中的中间件实现和流水线模型
694

被折叠的 条评论
为什么被折叠?



