明白了,原來 Go Web 框架中的中間件都是這樣實現的

2019-12-22     Go語言中文網

作者:鳥窩 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)}

參考文檔

  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
  16. https://gist.github.com/saelo/4190b75724adc06b1c5a
文章來源: https://twgreatdaily.com/zh-cn/k9i7L28BMH2_cNUgARI0.html