19.中间件、装饰器设计模式

本文介绍了在Golang中使用装饰器模式实现的中间件,包括责任链模式数组形式和装饰器链式调用,展示了如何在HTTP服务和RPC中应用,以及Gin框架中的中间件实现和流水线模型
摘要由CSDN通过智能技术生成

代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/13-middleware

一、中间件模式常见写法

不管是在http服务还是rpc服务中,我们都经常会用到中间件来做一些前置工作,如参数校验、过滤、限流、限频等,而中间件的写法基本是固定的两种,第一种是职责链模式的数组形式,在本人行为型之职责链模式一文中介绍过了,这里要介绍的是第二种常用写法(装饰器模式实现的链表模式)。

首先先定义好中间件的类型,这里我就简单定义为以下的格式,实际工作中基本也是如下格式,只是reqresp可能是具体的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函数将 handlermiddleware 聚合到一起,实现了让这个简单的Web Server中间件用法跟 Gin 框架用法相似的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值