Golang 函數執行時間統計裝飾器的一個實現

背景

最近在搭一個新項目的架子,在生產環境中,爲了能實時的監控程序的運行狀態,少不了邏輯執行時間長度的統計。時間統計這個功能實現的指望有下面幾點:html

  1. 實現細節要剝離:時間統計實現的細節不指望在顯式的寫在主邏輯中。由於主邏輯中的其餘邏輯和時間統計的抽象層次不在同一個層級
  2. 用於時間統計的代碼可複用
  3. 統計出來的時間結果是可被處理的。
  4. 對併發編程友好

實現思路

統計細節的剝離

最樸素的時間統計的實現,多是下面這個樣子:編程

func f() {
  startTime := time.Now()
  logicStepOne()
  logicStepTwo()
  endTime := time.Now()
  timeDiff := timeDiff(startTime, endTime)
  log.Info("time diff: %s", timeDiff)
}

《代碼整潔之道》告訴咱們:一個函數裏面的全部函數調用都應該處於同一個抽象層級。併發

在這裏時間開始、結束的獲取,使用時間的求差,屬於時間統計的細節,首先他不屬於主流程必要的一步,其次他們使用的函數 time.Now() 和 logicStepOne, logicStepTwo 並不在同一個抽象層級。函數

所以比較好的作法應該是把時間統計放在函數 f 的上層,好比:單元測試

func doFWithTimeRecord() {
  startTime: = time.Now()
  f()
  endTime := Time.Now()
  timeDiff := timeDIff(startTime, endTime)
  log.Info("time diff: %s", timeDiff)
}

時間統計代碼可複用&統計結果可被處理&不影響原函數的使用方式

咱們雖然達成了函數內抽象層級相同的目標,可是你們確定也能感覺到:這個函數並很差用。測試

緣由在於,咱們把要調用的函數 f 寫死在了 doFWithTimeRecord 函數中。這意味着,每個要統計時間的函數,我都須要實現一個 doXXWithTimeRecord, 而這些函數裏面的邏輯是相同的,這就違反了咱們 DRY(Don't Repeat Yourself)原則。所以爲了實現邏輯的複用,我認爲裝飾器是比較好的實現方式:將要執行的函數做爲參數傳入到時間統計函數中。設計

舉個網上看到的例子

實現一個功能,第一反應確定是查找同行有沒有現成的輪子。不過看了下,沒有達到本身的指望,舉個例子:code

type SumFunc func(int64, int64) int64

func timedSumFunc(f SumFunc) SumFunc {
  return func(start, end int64) int64 {
    defer func(t time.Time) {
      fmt.Printf("--- Time Elapsed: %v ---\n", time.Since(t))
    }(time.Now())
    
    return f(start, end)
  }
}

說說這段代碼很差的地方:orm

  1. 這個裝飾器入參寫死了函數的類型:htm

    type SumFunc func(int64, int64) int64

    也就是說,只要換一個函數,這個裝飾器就不能用了,這不符合咱們的第2點要求

  2. 這裏時間統計結果直接打印到了標準輸出,也就是說這個結果是不能被原函數的調用方去使用的:由於只有掉用方,才知道這個結果符不符合預期,是花太多時間了,仍是正常現象。這不符合咱們的第3點要求。

怎麼解決這兩個問題呢?

這個時候,《重構,改善既有代碼的設計》告訴咱們:Replace Method with Method Obejct——以函數對象取代函數。他的意思是當一個函數有比較複雜的臨時變量時,咱們能夠考慮將函數封裝成一個類。這樣咱們的函數就統一成了 0 個參數。(固然,本來就是做爲一個 struct 裏面的方法的話就適當作調整就行了)

如今,咱們的代碼變成了這樣:

type TimeRecorder interface {
  SetCost(time.Duration)
  TimeCost() time.Duration
}

func TimeCostDecorator(rec TimeRecorder, f func()) func() {
  return func() {
    startTime := time.Now()
    f()
    endTime := time.Now()
    timeCost := endTime.Sub(startTime)
    rec.SetCost(timeCost)
  }
}

這裏入參寫成是一個 interface ,目的是容許各類函數對象入參,只須要實現了 SetCost 和 TimeCost 方法便可

對併發編程友好

最後須要考慮的一個問題,不少時候,一個類在整個程序的生命週期是一個單例,這樣在 SetCost 的時候,就須要考慮併發寫的問題。這裏考慮一下幾種解決方案:

  1. 使用裝飾器配套的時間統計存儲對象,實現以下:

    func NewTimeRecorder() TimeRecorder {
      return &timeRecorder{}
    }
    
    type timeRecorder struct {
      cost time.Duration
    }
    
    func (tr *timeRecorder) SetCost(cost time.Duration) {
      tr.cost = cost
    }
    
    func (tr *timeRecorder) Cost() time.Duration {
      return tr.cost
    }
  2. 抽離出存粹的執行完就能夠銷燬的函數對象,每次要操做的時候都 new 一下
  3. 函數對象內部對 SetCost 函數實現鎖機制

這三個方案是按推薦指數從高到低排序的,由於我我的認爲:資源容許的狀況下,儘可能保持對象不可變;同時怎麼統計、存儲使用時長實際上是統計時間模塊本身的事情。

單元測試

最後補上單元測試:

func TestTimeCostDecorator(t *testing.T) {
  testFunc := func() {
    time.Sleep(time.Duration(1) * time.Second)
  }
  
  type args struct {
    rec TimeRecorder
    f func()
  }
  
  tests := []struct {
    name string
    args args
  }{
    {
      "test time cost decorator",
      args{
        NewTimeRecorder(),
        testFunc,
      },
    },
  }
  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      got := TimeCostDecorator(tt.args.rec, tt.args.f)
      got()
      if tt.args.rec.Cost().Round(time.Second) != time.Duration(1) * time.Second.Round(time.Second) {
        "Record time cost abnormal, recorded cost: %s, real cost: %s",
        tt.args.rec.Cost().String(),
        tt.Duration(1) * time.Second,
      }
    }) 
  }
}

測試經過,驗證了時間統計是沒問題的。至此,這個時間統計裝飾器就介紹完了。若是這個實現有什麼問題,或者你們有更好的實現方式,歡迎你們批評指正與提出~

原文地址:https://blog.coordinate35.cn/...

相關文章
相關標籤/搜索