作者:鸟窝 smallnest
原文链接:https://colobu.com/2019/08/21/decorator-pattern-pipeline-pattern-and-go-web-middlewares/
这篇文章想谈谈 Go 的装饰器模式、pipeline(filter)模式以及常见 web 框架中的中间件的实现方式。
修饰模式
修饰模式是常见的一种设计模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
有时候我把它叫做洋葱模式,洋葱内部最嫩最核心的时原始对象,然后外面包了一层业务逻辑,然后还可以继续在外面包一层业务逻辑。
原理是:增加一个修饰类包裹原来的类,包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能,但是,在不需要用到新功能的地方,它可以直接调用原来的类中的方法。与适配器模式不同,装饰器模式的修饰类必须和原来的类有相同的接口。它是在运行时动态的增加对象的行为,在执行包裹对象的前后执行特定的逻辑,而代理模式主要控制内部对象行为,一般在编译器就确定了。
对于 Go 语言来说,因为函数是第一类的,所以包裹的对象也可以是函数,比如最常见的时 http 的例子:
func log(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Before") h.ServeHTTP(w, r) log.Println("After") })}
上面提供了打印日志的修饰器,在执行实际的包裹对象前后分别打印出一条日志。因为 http 标准库既可以以函数的方式(http.HandlerFunc)提供 router,也可以以 struct 的方式(http.Handler)提供,所以这里提供了两个修饰器(修饰函数)的实现。
泛型修饰器
左耳朵耗子在他的Go 语言的修饰器编程一文中通过反射的方式,提供了类泛型的修饰器方式:
func Decorator(decoPtr, fn interface{}) (err error) { var decoratedFunc, targetFunc reflect.Value decoratedFunc = reflect.ValueOf(decoPtr).Elem() targetFunc = reflect.ValueOf(fn) v := reflect.MakeFunc(targetFunc.Type(), func(in []reflect.Value) (out []reflect.Value) { fmt.Println("before") out = targetFunc.Call(in) fmt.Println("after") return }) decoratedFunc.Set(v) return}
stackoverflow 也有一篇问答提供了反射实现类泛型的装饰器模式,基本都是类似的。
func Decorate(impl interface{}) interface{} { fn := reflect.ValueOf(impl) //What does inner do ? What is this codeblock ? inner := func(in []reflect.Value) []reflect.Value { //Why does this return the same type as the parameters passed to the function ? Does this mean this decorator only works for fns with signature func (arg TypeA) TypeA and not func (arg TypeA) TypeB ? f := reflect.ValueOf(impl) fmt.Println("Stuff before") // ... ret := f.Call(in) //What does call do ? Why cant we just use f(in) ? fmt.Println("Stuff after") // ... return ret } v := reflect.MakeFunc(fn.Type(), inner) return v.Interface()}
当然最早 14 年的时候 saelo 就提供了这个gist,居然是零 star,零 fork,我贡献一个 fork。
pipeline 模式
职责链是 GoF 23 种设计模式的一种,它包含一组命令和一系列的处理对象(handler),每个处理对象定义了它要处理的命令的业务逻辑,剩余的命令交给其它处理对象处理。所以整个业务你看起来就是if ... else if ... else if ....... else ... endif 这样的逻辑判断,handler 还负责分发剩下待处理的命令给其它 handler。职责链既可以是直线型的,也可以复杂的树状拓扑。
pipeline 是一种架构模式,它由一组处理单元组成的链构成,处理单元可以是进程、线程、纤程、函数等构成,链条中的每一个处理单元处理完毕后就会把结果交给下一个。处理单元也叫做 filter,所以这种模式也叫做pipes and filters design pattern。
和职责链模式不同,职责链设计模式不同,职责链设计模式是一个行为设计模式,而 pipeline 是一种架构模式;其次 pipeline 的处理单元范围很广,大到进程小到函数,都可以组成 pipeline 设计模式;第三狭义来讲 pipeline 的处理方式是单向直线型的,不会有分叉树状的拓扑;第四是针对一个处理数据,pipeline 的每个处理单元都会处理参与处理这个数据(或者上一个处理单元的处理结果)。
pipeline 模式经常用来实现中间件,比如 java web 中的 filter, Go web 框架中的中间件。接下来让我们看看 Go web 框架的中间件实现的各种技术。
Go web 框架的中间件
这一节我们看看由哪些方式可以实现 web 框架的中间件。
这里我们说的 web 中间件是指在接收到用户的消息,先进行一系列的预处理,然后再交给实际的http.Handler去处理,处理结果可能还需要一系列的后续处理。
虽说,最终各个框架还是通过修饰器的方式实现 pipeline 的中间件,但是各个框架对于中间件的处理还是各有各的风格,区别主要是When、Where和How。
初始化时配置
通过上面一节的修饰器模式,我们可以实现 pipeline 模式。
看看谢大的beego框架中的实现:
// MiddleWare function for http.Handlertype MiddleWare func(http.Handler) http.Handler// Run beego application.func (app *App) Run(mws ...MiddleWare) { ...... app.Server.Handler = app.Handlers for i := len(mws) - 1; i >= 0; i-- { if mws[i] == nil { continue } app.Server.Handler = mws[i](app.Server.Handler) } ......}
在程序启动的时候就将中间件包装好,然后应用只需要最终持有最终的 handler 即可。
使用 filter 数组实现
一些 Go web 框架是使用 filter 数组(严格讲是 slice)实现了中间件的技术。
我们看一下gin框架实现中间件的例子。
数据结构:
gin.go
// HandlersChain defines a HandlerFunc array.type HandlersChain []HandlerFunc// Last returns the last handler in the chain. ie. the last handler is the main own.func (c HandlersChain) Last() HandlerFunc {if length := len(c); length > 0 {return c[length-1]}return nil}
配置:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {group.Handlers = append(group.Handlers, middleware...)return group.returnObj()}
因为中间件也是 HandlerFunc, 可以当作一个 handler 来处理。
我们再看看echo框架实现中间件的例子。
echo.go
// Pre adds middleware to the chain which is run before router.func (e *Echo) Pre(middleware ...MiddlewareFunc) {e.premiddleware = append(e.premiddleware, middleware...)}// Use adds middleware to the chain which is run after router.func (e *Echo) Use(middleware ...MiddlewareFunc) {e.middleware = append(e.middleware, middleware...)}func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {// Acquire contextc := e.pool.Get().(*context)c.Reset(r, w)h := NotFoundHandlerif e.premiddleware == nil {e.findRouter(r.Host).Find(r.Method, getPath(r), c)h = c.Handler()h = applyMiddleware(h, e.middleware...)} else {h = func(c Context) error {e.findRouter(r.Host).Find(r.Method, getPath(r), c)h := c.Handler()h = applyMiddleware(h, e.middleware...)return h(c)}h = applyMiddleware(h, e.premiddleware...) } ......}
echo 框架在处理每一个请求的时候,它是实时组装 pipeline 的,这也意味着你可以动态地更改中间件的使用。
使用链表的方式实现
iris框架使用链接的方式实现
type WrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRouter http.HandlerFunc)func (router *Router) WrapRouter(wrapperFunc WrapperFunc) {if wrapperFunc == nil {return}router.mu.Lock()defer router.mu.Unlock()if router.wrapperFunc != nil {// wrap into one function, from bottom to top, end to begin.nextWrapper := wrapperFuncprevWrapper := router.wrapperFuncwrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {if next != nil {nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) {prevWrapper(_w, _r, next)})nextWrapper(w, r, nexthttpFunc)}}}router.wrapperFunc = wrapperFunc}
可以看到 iris 这种方式和 beego 基本类似,区别是它可以针对不同的 Router 进行不同装饰,也可以在运行的时候动态的添加装饰器,但是不能动态地删除。
函数式实现
这个方式基本就是链表的方式,和 iris 这种方式实现的区别就是它实现了链式调用的功能。
链式调用的功能在很多地方都有用,比如 Builder 设计模式中最常用用来链式调用进行初始化设置,我们也可以用它来实现链式的连续的装饰器包裹。
我年初的印象中看到过一篇文章介绍这个方式,但是在写这篇文章的搜索了两天也没有找到印象中的文章,所以我自己写了一个装饰器的链式调用,只是进行了原型的还是,并没有实现 go web 框架中间件的实现,其实上面的各种中间件的实现方式已经足够好了。
在 Go 语言中, 函数是第一类的,你可以使用高阶函数把函数作为参数或者返回值。
函数本身也也可以有方法,这一点就有意思,可以利用这个特性实现函数式的链式调用。
比如下面的例子,
type Fn func(x, y int) intfunc (fn Fn) Chain(f Fn) Fn { return func(x, y int) int { fmt.Println(fn(x, y)) return f(x, y) }}func add(x, y int) int { fmt.Printf("%d + %d = ", x, y) return x + y}func minus(x, y int) int { fmt.Printf("%d - %d = ", x, y) return x - y}func mul(x, y int) int { fmt.Printf("%d * %d = ", x, y) return x * y}func divide(x, y int) int { fmt.Printf("%d / %d = ", x, y) return x / y}func main() { var result = Fn(add).Chain(Fn(minus)).Chain(Fn(mul)).Chain(Fn(divide))(10, 5) fmt.Println(result)}
参考文档
- https://en.wikipedia.org/wiki/Decorator_pattern
- https://stackoverflow.com/questions/27174703/difference-between-pipe-filter-and-chain-of-responsibility
- https://docs.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters
- https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
- https://stackoverflow.com/questions/45395861/a-generic-golang-decorator-clarification-needed-for-a-gist
- https://github.com/alex-leonhardt/go-decorator-pattern
- https://coolshell.cn/articles/17929.html
- https://www.bartfokker.nl/posts/decorators/
- https://drstearns.github.io/tutorials/gomiddleware/
- https://preslav.me/2019/07/07/implementing-a-functional-style-builder-in-go/
- https://en.wikipedia.org/wiki/Pipeline_(software)
- https://github.com/gin-gonic/gin/blob/461df9320ac22d12d19a4e93894c54dd113b60c3/gin.go#L31
- https://github.com/gin-gonic/gin/blob/4a23c4f7b9ced6b8e4476f2e021a61165153b71d/routergroup.go#L51
- https://github.com/labstack/echo/blob/master/echo.go#L382
- https://github.com/kataras/iris/blob/master/core/router/router.go#L131
- https://gist.github.com/saelo/4190b75724adc06b1c5a