Golang 常見設計模式之裝飾模式

撈起月亮的漁民發表於2022-09-08

想必只要是熟悉 Python 的同學對裝飾模式一定不會陌生,這類 Python 從語法上原生支援的裝飾器,大大提高了裝飾模式在 Python 中的應用。儘管 Go 語言中裝飾模式沒有 Python 中應用的那麼廣泛,但是它也有其獨到的地方。接下來就一起看下裝飾模式在 Go 語言中的應用。

 簡單裝飾器

我們通過一個簡單的例子來看一下裝飾器的簡單應用,首先編寫一個 hello 函式:


package main

import "fmt"

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

func main() {
    hello()
}

完成上面程式碼後,執行會輸出“Hello World!”。接下來通過以下方式,在列印“Hello World!”前後各加一行日誌:

package main

import "fmt"

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

func main() {
    hello()
}

程式碼執行後輸出:

before
Hello World!
after

當然我們可以選擇一個更好的實現方式,即單獨編寫一個專門用來列印日誌的 logger 函式,示例如下:

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 函式接收並返回了一個函式,且引數和返回值的函式簽名同 hello 一樣。然後我們在原來呼叫 hello() 的位置進行如下修改:

hello := logger(hello)
hello()

這樣我們通過 logger 函式對 hello 函式的包裝,更加優雅的實現了給 hello 函式增加日誌的功能。執行後的列印結果仍為:

before
Hello World!
after

其實 logger 函式也就是我們在 Python 中經常使用的裝飾器,因為 logger 函式不僅可以用於 hello,還可以用於其他任何與 hello 函式有著同樣簽名的函式。

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


package main

import "fmt"

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

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

func main() {
    // hello 函式呼叫方式不變
    hello()
}

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

裝飾器實現中介軟體

儘管 Go 語言中裝飾器的寫法不如 Python 語言精簡,但它被廣泛運用於 Web 開發場景的中介軟體元件中。比如 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...) 的方式給路由增加非常多的中介軟體,來方便我們攔截路由處理函式,並在其前後分別做一些處理邏輯。

而 Gin 框架的中介軟體正是使用裝飾模式來實現的。下面我們借用 Go 語言自帶的 http 庫進行一個簡單模擬。這是一個簡單的 Web Server 程式,其監聽 8888 埠,當訪問 /hello 路由時會進入 handleHello 函式邏輯:

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 函式對 handleHello 進行了包裝,使其支援列印訪問日誌和認證校驗功能。如果我們還需要加入其他中介軟體攔截功能,可以通過這種方式進行無限包裝。

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

對結果進行簡單分析可以看到,第一次請求 /hello 介面時,由於沒有攜帶認證 token,收到了 unauthorized 響應。第二次請求時攜帶了 token,則得到響應“Hello World!”,並且後臺程式列印如下日誌:

before
handle hello
after

這說明中介軟體執行順序是先由外向內進入,再由內向外返回。而這種一層一層包裝處理邏輯的模型有一個非常形象且貼切的名字,洋蔥模型。

但用洋蔥模型實現的中介軟體有一個直觀的問題。相比於 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
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 函式將 handler 和 middleware 聚合到一起,實現了讓這個簡單的 Web Server 中介軟體用法跟 Gin 框架用法相似的效果。

再次啟動 Server 進行驗證:

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

總結

簡單瞭解了 Go 語言中如何實現裝飾模式後,我們通過一個 Web Server 程式中介軟體,學習了裝飾模式在 Go 語言中的應用。

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

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

推薦閱讀

兩招提升硬碟儲存資料的寫入效率

【程式設計師的實用工具推薦】 Mac 效率神器 Alfred

相關文章