明白了,原来Go web框架中的中间件都是这样实现的

smallnest · · 1016 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

这篇文章想谈谈Go的装饰器模式、pipeline(filter)模式以及常见web框架中的中间件的实现方式。

修饰模式

修饰模式是常见的一种设计模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

有时候我把它叫做洋葱模式,洋葱内部最嫩最核心的时原始对象,然后外面包了一层业务逻辑,然后还可以继续在外面包一层业务逻辑。

原理是:增加一个修饰类包裹原来的类,包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能,但是,在不需要用到新功能的地方,它可以直接调用原来的类中的方法。与适配器模式不同,装饰器模式的修饰类必须和原来的类有相同的接口。它是在运行时动态的增加对象的行为,在执行包裹对象的前后执行特定的逻辑,而代理模式主要控制内部对象行为,一般在编译器就确定了。

对于Go语言来说,因为函数是第一类的,所以包裹的对象也可以是函数,比如最常见的时http的例子:

1
2
3
4
5
6
7
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语言的修饰器编程一文中通过反射的方式,提供了类泛型的修饰器方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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也有一篇问答提供了反射实现类泛型的装饰器模式,基本都是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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的中间件,但是各个框架对于中间件的处理还是各有各的风格,区别主要是WhenWhereHow

初始化时配置

通过上面一节的修饰器模式,我们可以实现pipeline模式。

看看谢大的beego框架中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MiddleWare function for http.Handler
type 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
1
2
3
4
5
6
7
8
9
10
// 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
}

配置:

1
2
3
4
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}

因为中间件也是HandlerFunc, 可以当作一个handler来处理。

我们再看看echo框架实现中间件的例子。

echo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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 context
c := e.pool.Get().(*context)
c.Reset(r, w)
h := NotFoundHandler
if 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框架使用链接的方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 := wrapperFunc
prevWrapper := router.wrapperFunc
wrapperFunc = 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语言中, 函数是第一类的,你可以使用高阶函数把函数作为参数或者返回值。

函数本身也也可以有方法,这一点就有意思,可以利用这个特性实现函数式的链式调用。

比如下面的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Fn func(x, y int) int
func (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)
}

参考文档

  1. https://en.wikipedia.org/wiki/Decorator_pattern
  2. https://stackoverflow.com/questions/27174703/difference-between-pipe-filter-and-chain-of-responsibility
  3. https://docs.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters
  4. https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
  5. https://stackoverflow.com/questions/45395861/a-generic-golang-decorator-clarification-needed-for-a-gist
  6. https://github.com/alex-leonhardt/go-decorator-pattern
  7. https://coolshell.cn/articles/17929.html
  8. https://www.bartfokker.nl/posts/decorators/
  9. https://drstearns.github.io/tutorials/gomiddleware/
  10. https://preslav.me/2019/07/07/implementing-a-functional-style-builder-in-go/
  11. https://en.wikipedia.org/wiki/Pipeline_(software)
  12. https://github.com/gin-gonic/gin/blob/461df9320ac22d12d19a4e93894c54dd113b60c3/gin.go#L31
  13. https://github.com/gin-gonic/gin/blob/4a23c4f7b9ced6b8e4476f2e021a61165153b71d/routergroup.go#L51
  14. https://github.com/labstack/echo/blob/master/echo.go#L382
  15. https://github.com/kataras/iris/blob/master/core/router/router.go#L131

有疑问加站长微信联系(非本文作者)

本文来自:鸟窝

感谢作者:smallnest

查看原文:明白了,原来Go web框架中的中间件都是这样实现的

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1016 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传