Golang 常见设计模式之装饰模式
想必只要是熟悉 Python 的同學對裝飾模式一定不會陌生,這類 Python 從語法上原生支持的裝飾器,大大提高了裝飾模式在 Python 中的應用。盡管 Go 語言中裝飾模式沒有 Python 中應用的那么廣泛,但是它也有其獨到的地方。接下來就一起看下裝飾模式在 Go 語言中的應用。
## 簡單裝飾器
我們通過一個簡單的例子來看一下裝飾器的簡單應用,首先編寫一個 hello 函數:
package mainimport "fmt"func hello() {fmt.Println("Hello World!") }func main() {hello() }完成上面代碼后,執行會輸出“Hello World!”。接下來通過以下方式,在打印“Hello World!”前后各加一行日志:
package mainimport "fmt"func hello() {fmt.Println("before")fmt.Println("Hello World!")fmt.Println("after") }func main() {hello() }代碼執行后輸出:
before Hello World! after當然我們可以選擇一個更好的實現方式,即單獨編寫一個專門用來打印日志的 logger 函數,示例如下:
package mainimport "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 mainimport "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 mainimport "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 mainimport ("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 mainimport ("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 框架有所幫助。
總結
以上是生活随笔為你收集整理的Golang 常见设计模式之装饰模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自媒体时代的贤内助——AI 视频云
- 下一篇: OpenShift 与 OpenStac