編程模式之 Go 如何實現裝飾器

前言

哈嘍,大家好,我是asong。今天想與大家聊一聊如何用Go實現裝飾器代碼。爲什麼會有這個想法呢?最近由於項目需要一直在看python的代碼,在這個項目中應用了大量的裝飾器代碼,一個裝飾器代碼可以在全文共用,減少了冗餘代碼。python的語法糖讓實現裝飾器變得很簡單,但是Go語言的不多,而且又是強類型的靜態無虛擬機的語言,所以,沒有辦法做到像JavaPython 那樣寫出優雅的裝飾器的代碼,但也是可以實現的,今天我們就看看如何Go語言寫出裝飾器代碼!

什麼是裝飾器

介紹裝飾器基本概念之前,我們先舉個例子,跟裝飾器很貼切:

如今我們的生活水平提高了,基本人手一臺手機,大家也知道手機屏幕摔到地板上是很容易碎屏的,手機屏幕一壞,又要多花一筆費用進行維修,很是心痛;那麼有什麼什麼辦法來避免這個問題呢,在不破壞手機屏幕結構的情況下,讓我們的手機更耐壞呢?其實我們只需要花幾元錢買一個鋼化膜,鋼化膜在不改變原有手機屏幕的結構下,讓手機變得更耐摔了。

根據上面這個例子,就可以引出本文的核心 -> 裝飾器。裝飾器本質就是:

函數裝飾器用於在源碼中 “標記” 函數,以某種方式增強函數的行爲。

裝飾器是一個強大的功能,但是若想掌握,必須要理解閉包!閉包的概念我們在下面一小節說明,我們先來看一看python是如何使用裝飾器的:

def metric(fn):
    @functools.wraps(fn)
    def timer(*arag, **kw):
        start = time.time()
        num = fn(*arag, **kw)
        end = time.time()
        times = (end - start) * 1000
        print('%s executed in %s ms' % (fn.__name__, times))
        return num
    return timer

@metric
def Sum(x, y):
    time.sleep(0.0012)
    return x + y;


Sum(10, 20)

這裏要實現功能很簡單,metric就是一個裝飾器函數,他可以作用於任何函數之上,並打印該函數的執行時間,有個這個裝飾器,我們想要知道任何一個函數的執行時間,就簡便很多了。

簡單總結一下裝飾器使用場景:

裝飾器的使用場景還用很多,就不一一列舉了,下面我們就來看看如何使用Go也來實現裝飾器代碼吧!

閉包

裝飾器的實現和閉包是分不開的,所以我們先來學習一下什麼是閉包!

我們通常會把閉包和匿名函數弄混,這是因爲:在 函數內部定義函數不常見,直到開始使用匿名函數纔會這樣做。而且, 只有涉及嵌套函數時纔有閉包問題。因此,很多人是同時知道這兩個概念的。

其實,閉包指延伸了作用域的函數,其中包含函數定義體中引用、但是不在定義體中定義的非全局變量。函數是不是匿名的沒有關係,關鍵是 它能訪問定義體之外定義的非全局變量。

光看概念其實挺難理解閉包,我們通過例子來進行理解。

func makeAverager() func(val float32) float32{
 series := make([]float32,0)
 return func(val float32) float32 {
  series = append(series, val)
  total := float32(0)
  for _,v:=range series{
   total +=v
  }
  return total/ float32(len(series))
 }
}

func main() {
 avg := makeAverager()
 fmt.Println(avg(10))
 fmt.Println(avg(30))
}

這個例子,你猜運行結果是什麼?10,30還是10,20

運行一下,答案出來了:10,20。爲什麼會這樣呢?我們來分析一下!

上面的代碼中makeAverager的寫法在C語言中是不允許的,因爲在C語言中,函數內的內存分配是在棧上的,在makeAverager返回後,這部分棧就被回收了,但是在Go語言中是沒有問題的,因爲Go語言會進行escape analyze分析出變量的作用範圍,將變量在堆上進行內存分配,我們使用go build --gcflags=-m ./test/test1.go來看一下分析結果:

# command-line-arguments
test/test1.go:21:13: inlining call to fmt.Println
test/test1.go:22:13: inlining call to fmt.Println
test/test1.go:8:2: moved to heap: series
test/test1.go:8:16: make([]float32, 0) escapes to heap
test/test1.go:9:9: func literal escapes to heap
test/test1.go:21:17: avg(10) escapes to heap
test/test1.go:21:13: []interface {} literal does not escape
test/test1.go:22:17: avg(30) escapes to heap
test/test1.go:22:13: []interface {} literal does not escape
<autogenerated>:1: .this does not escape

從運行結果我們可以看出,seriesfuncavg都逃逸到了堆上。所以我們可以得出結論,series變量和func(val float32) float32{}被引用後,他所在的函數結束,也不會馬上銷燬,這也是變相延長了函數的生命週期!

小結:綜上所訴,閉包是一種函數,它會保留定義函數時存在的自由變量的綁定, 這樣調用函數時,雖然定義作用域不可用了,但是仍能使用那些綁定。

注意,只有嵌套在其他函數中的函數纔可能需要處理不在全局作用域中 的外部變量。

Gin 中裝飾器的應用

大家應該都使用過Gin這個Web框架,其在註冊路由時提供了中間件的使用,可以攔截 http 請求 - 響應生命週期的特殊函數,在請求 - 響應生命週期中可以註冊多箇中間件,每個中間件執行不同的功能,一箇中間執行完再輪到下一個中間件執行。這個中間件其實就是使用的裝飾器,我們來看一件簡單的例子:

func VerifyHeader() gin.HandlerFunc {
 return func(c *gin.Context) {
  header := c.Request.Header.Get("token")
  if header == "" {
   c.JSON(200, gin.H{
    "code":   1000,
    "msg":    "Not logged in",
   })
   return
  }
 }
}
func main()  {
 r := gin.Default()
 group := r.Group("/api/asong",VerifyHeader())
 {
  group.GET("/ping", func(context *gin.Context) {
   context.JSON(200,gin.H{
    "message""pong",
   })
  })
 }
 r.Run()
}

這段代碼很簡單,我們只需要寫一個VerifyHeader函數,在註冊路由的時候添加進去就可以了,當有請求進來時,會先執行gin.HanderFunc函數,在Gin框架中使用一個切片來存儲的,所以在添加中間件時,要注意添加順序哦!

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 finalSize := len(group.Handlers) + len(handlers)
 if finalSize >= int(abortIndex) {
  panic("too many handlers")
 }
 mergedHandlers := make(HandlersChain, finalSize)
 copy(mergedHandlers, group.Handlers)
 copy(mergedHandlers[len(group.Handlers):], handlers)
 return mergedHandlers
}

net/http 使用裝飾器

上面我們看到了裝飾器在Gin框架中的應用,這種設計大大減少了冗餘代碼的出現,也使代碼的可擴展性提高了。那麼接下來我們就在標準庫http包上自己實現一個裝飾器,練習一下。

我們知道Go語言的http標準庫是不能使用中間件的,所以我們的機會來了,我們來給他實現一個!看代碼:

type DecoratorHandler func(http.HandlerFunc) http.HandlerFunc

func MiddlewareHandlerFunc(hp http.HandlerFunc, decors ...DecoratorHandler) http.HandlerFunc {
 for d := range decors {
  dp := decors[len(decors)-1-d]
  hp = dp(hp)
 }
 return hp
}

func VerifyHeader(h http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  token := r.Header.Get("token")
  if token == "" {
   fmt.Fprintf(w,r.URL.Path +" response: Not Logged in")
   return
  }
  h(w,r)
 }
}

func Pong(w http.ResponseWriter, r *http.Request)  {
 fmt.Fprintf(w,r.URL.Path +"response: pong")
 return
}


func main()  {
 http.HandleFunc("/api/asong/ping",MiddlewareHandlerFunc(Pong,VerifyHeader))
 err := http.ListenAndServe(":8080", nil)
 if err != nil {
  log.Fatal("ListenAndServe: ", err)
 }
}

實現起來還是比較簡單,這裏重新聲明瞭DecoratorHandler類型,本質就是func(http.HandlerFunc) http.HandlerFunc,這樣更加方便我們添加中間件函數,中間件按照添加的順序執行。

總結

好啦,本文到這裏就結束了,這一文我們學習了閉包的概念,通過閉包我們學習瞭如何在Go語言中使用裝飾器,因爲Go語言中不支持註解這個語法糖,所以使用裝飾器還是有點醜陋的,不過這個思想還是挺重要的,我們日常開發中可以參考這種思想,寫出更優質的代碼來!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/B_VYr3I525-vjHgzfW3Jhg