Golang 常見設(shè)計模式之裝飾模式

想必只要是熟悉 Python 的同學(xué)對裝飾模式一定不會陌生,這類 Python 從語法上原生支持的裝飾器,大大提高了裝飾模式在 Python 中的應(yīng)用。盡管 Go 語言中裝飾模式?jīng)]有 Python 中應(yīng)用的那么廣泛,但是它也有其獨到的地方。接下來就一起看下裝飾模式在 Go 語言中的應(yīng)用。

簡單裝飾器

我們通過一個簡單的例子來看一下裝飾器的簡單應(yīng)用,首先編寫一個 hello 函數(shù):


package main

import "fmt"

func hello() {
    fmt.Println("Hello World!")
}

func main() {
    hello()
}

完成上面代碼后,執(zhí)行會輸出“Hello World!”。接下來通過以下方式,在打印“Hello World!”前后各加一行日志:

package main

import "fmt"

func hello() {
    fmt.Println("before")
    fmt.Println("Hello World!")
    fmt.Println("after")
}

func main() {
    hello()
}

代碼執(zhí)行后輸出:

before
Hello World!
after

當然我們可以選擇一個更好的實現(xiàn)方式,即單獨編寫一個專門用來打印日志的 logger 函數(shù),示例如下:

package main

import "fmt"

func logger(f func()) func() {
    return func() {
        fmt.Println("before")
        f()
        fmt.Println("after")
    }
}

func hello() {
    fmt.Println("Hello World!")
}

func main() {
    hello := logger(hello)
    hello()
}

可以看到 logger 函數(shù)接收并返回了一個函數(shù),且參數(shù)和返回值的函數(shù)簽名同 hello 一樣。然后我們在原來調(diào)用 hello() 的位置進行如下修改:

hello := logger(hello)
hello()

這樣我們通過 logger 函數(shù)對 hello 函數(shù)的包裝,更加優(yōu)雅的實現(xiàn)了給 hello 函數(shù)增加日志的功能。執(zhí)行后的打印結(jié)果仍為:

before
Hello World!
after

其實 logger 函數(shù)也就是我們在 Python 中經(jīng)常使用的裝飾器,因為 logger 函數(shù)不僅可以用于 hello,還可以用于其他任何與 hello 函數(shù)有著同樣簽名的函數(shù)。

當然如果想使用 Python 中裝飾器的寫法,我們可以這樣做:


package main

import "fmt"

func logger(f func()) func() {
    return func() {
        fmt.Println("before")
        f()
        fmt.Println("after")
    }
}

// 給 hello 函數(shù)打上 logger 裝飾器
@logger
func hello() {
    fmt.Println("Hello World!")
}

func main() {
    // hello 函數(shù)調(diào)用方式不變
    hello()
}

但很遺憾,上面的程序無法通過編譯。因為 Go 語言目前還沒有像 Python 語言一樣從語法層面提供對裝飾器語法糖的支持。

裝飾器實現(xiàn)中間件

盡管 Go 語言中裝飾器的寫法不如 Python 語言精簡,但它被廣泛運用于 Web 開發(fā)場景的中間件組件中。比如 Gin Web 框架的如下代碼,只要使用過就肯定會覺得熟悉:

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.Logger() 增加日志,使用 gin.Recovery() 來處理 panic 異常一樣,在 Gin 框架中可以通過 r.Use(middlewares...) 的方式給路由增加非常多的中間件,來方便我們攔截路由處理函數(shù),并在其前后分別做一些處理邏輯。

而 Gin 框架的中間件正是使用裝飾模式來實現(xiàn)的。下面我們借用 Go 語言自帶的 http 庫進行一個簡單模擬。這是一個簡單的 Web Server 程序,其監(jiān)聽 8888 端口,當訪問 /hello 路由時會進入 handleHello 函數(shù)邏輯:

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 != "fake_token" {
            _, _ = w.Write([]byte("unauthorized\n"))
            return
        }
        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))
}

我們分別使用 loggerMiddleware、authMiddleware 函數(shù)對 handleHello 進行了包裝,使其支持打印訪問日志和認證校驗功能。如果我們還需要加入其他中間件攔截功能,可以通過這種方式進行無限包裝。

啟動這個 Server 來驗證下裝飾器:

對結(jié)果進行簡單分析可以看到,第一次請求 /hello 接口時,由于沒有攜帶認證 token,收到了 unauthorized 響應(yīng)。第二次請求時攜帶了 token,則得到響應(yīng)“Hello World!”,并且后臺程序打印如下日志:

before
handle hello
after

這說明中間件執(zhí)行順序是先由外向內(nèi)進入,再由內(nèi)向外返回。而這種一層一層包裝處理邏輯的模型有一個非常形象且貼切的名字,洋蔥模型。

但用洋蔥模型實現(xiàn)的中間件有一個直觀的問題。相比于 Gin 框架的中間件寫法,這種一層層包裹函數(shù)的寫法不如 Gin 框架提供的 r.Use(middlewares...) 寫法直觀。

Gin 框架源碼的中間件和 handler 處理函數(shù)實際上被一起聚合到了路由節(jié)點的 handlers 屬性中。其中 handlers 屬性是 HandlerFunc 類型切片。對應(yīng)到用 http 標準庫實現(xiàn)的 Web Server 中,就是滿足 func(ResponseWriter, *Request) 類型的 handler 切片。

當路由接口被調(diào)用時,Gin 框架就會像流水線一樣依次調(diào)用執(zhí)行 handlers 切片中的所有函數(shù),再依次返回。這種思想也有一個形象的名字,就叫作流水線(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
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 函數(shù)將 handler 和 middleware 聚合到一起,實現(xiàn)了讓這個簡單的 Web Server 中間件用法跟 Gin 框架用法相似的效果。

再次啟動 Server 進行驗證:

改造成功,跟之前使用洋蔥模型寫法的結(jié)果如出一轍。

總結(jié)

簡單了解了 Go 語言中如何實現(xiàn)裝飾模式后,我們通過一個 Web Server 程序中間件,學(xué)習(xí)了裝飾模式在 Go 語言中的應(yīng)用。

需要注意的是,盡管 Go 語言實現(xiàn)的裝飾器有類型上的限制,不如 Python 裝飾器那般通用。就像我們最終實現(xiàn)的 pipelineHandlers 不如 Gin 框架中間件強大,比如不能延遲調(diào)用,通過 c.Next() 控制中間件調(diào)用流等。但不能因為這樣就放棄,因為 GO 語言裝飾器依然有它的用武之地。

Go 語言是靜態(tài)類型語言不像 Python 那般靈活,所以在實現(xiàn)上要多費一點力氣。希望通過這個簡單的示例,相信對大家深入學(xué)習(xí) Gin 框架有所幫助。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 所謂框架 框架一直是敏捷開發(fā)中的利器,能讓開發(fā)者很快的上手并做出應(yīng)用,甚至有的時候,脫離了框架,一些開發(fā)者都不會寫...
    人世間閱讀 217,096評論 11 242
  • 本來自己打算繼續(xù)學(xué)下beanFactory源碼的,但是放假了自己也沒什么精神,看源碼又要求注意力很集中,所以想著看...
    me_2f11閱讀 1,368評論 1 1
  • 本來自己打算繼續(xù)學(xué)下beanFactory源碼的,但是放假了自己也沒什么精神,看源碼又要求注意力很集中,所以想著看...
    非典型_程序員閱讀 60,361評論 3 20
  • 轉(zhuǎn)發(fā)自:http://shanshanpt.github.io/2016/05/03/go-gin.html gi...
    dncmn閱讀 6,213評論 0 1
  • 學(xué)生時代曾和幾個朋友做了一個筆記本小應(yīng)用,當時我的角色是pm + dba,最近心血來潮,想把這個玩意自己實現(xiàn)一遍,...
    一根薯條閱讀 636評論 0 0

友情鏈接更多精彩內(nèi)容