Golang 函數(shù)執(zhí)行時(shí)間統(tǒng)計(jì)裝飾器的一個(gè)實(shí)現(xiàn)

背景

在生產(chǎn)環(huán)境中纠吴,為了能實(shí)時(shí)的監(jiān)控程序的運(yùn)行狀態(tài)戴已,少不了邏輯執(zhí)行時(shí)間長(zhǎng)度的統(tǒng)計(jì)糖儡。時(shí)間統(tǒng)計(jì)這個(gè)功能實(shí)現(xiàn)的期望有下面幾點(diǎn):

  1. 實(shí)現(xiàn)細(xì)節(jié)要?jiǎng)冸x:時(shí)間統(tǒng)計(jì)實(shí)現(xiàn)的細(xì)節(jié)不期望在顯式的寫在主邏輯中怔匣。因?yàn)橹鬟壿嬛械钠渌壿嫼蜁r(shí)間統(tǒng)計(jì)的抽象層次不在同一個(gè)層級(jí)
  2. 用于時(shí)間統(tǒng)計(jì)的代碼可復(fù)用
  3. 統(tǒng)計(jì)出來(lái)的時(shí)間結(jié)果是可被處理的桦沉。
  4. 對(duì)并發(fā)編程友好

實(shí)現(xiàn)思路

統(tǒng)計(jì)細(xì)節(jié)的剝離

最樸素的時(shí)間統(tǒng)計(jì)的實(shí)現(xiàn)纯露,可能是下面這個(gè)樣子:

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

《代碼整潔之道》告訴我們:一個(gè)函數(shù)里面的所有函數(shù)調(diào)用都應(yīng)該處于同一個(gè)抽象層級(jí)埠褪。

在這里時(shí)間開(kāi)始挤庇、結(jié)束的獲取,使用時(shí)間的求差渴语,屬于時(shí)間統(tǒng)計(jì)的細(xì)節(jié)驾凶,首先他不屬于主流程必要的一步,其次他們使用的函數(shù) time.Now() 和 logicStepOne, logicStepTwo 并不在同一個(gè)抽象層級(jí)狭郑。

因此比較好的做法應(yīng)該是把時(shí)間統(tǒng)計(jì)放在函數(shù) f 的上層,比如:

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

時(shí)間統(tǒng)計(jì)代碼可復(fù)用&統(tǒng)計(jì)結(jié)果可被處理&不影響原函數(shù)的使用方式

我們雖然達(dá)成了函數(shù)內(nèi)抽象層級(jí)相同的目標(biāo)脏答,但是大家肯定也能感受到:這個(gè)函數(shù)并不好用殖告。

原因在于,我們把要調(diào)用的函數(shù) f 寫死在了 doFWithTimeRecord 函數(shù)中羡洁。這意味著爽丹,每一個(gè)要統(tǒng)計(jì)時(shí)間的函數(shù)粤蝎,我都需要實(shí)現(xiàn)一個(gè) doXXWithTimeRecord, 而這些函數(shù)里面的邏輯是相同的,這就違反了我們 DRY(Don't Repeat Yourself)原則初澎。因此為了實(shí)現(xiàn)邏輯的復(fù)用,我認(rèn)為裝飾器是比較好的實(shí)現(xiàn)方式:將要執(zhí)行的函數(shù)作為參數(shù)傳入到時(shí)間統(tǒng)計(jì)函數(shù)中桑谍。

舉個(gè)網(wǎng)上看到的例子

實(shí)現(xiàn)一個(gè)功能霉囚,第一反應(yīng)肯定是查找同行有沒(méi)有現(xiàn)成的輪子匕积。不過(guò)看了下,沒(méi)有達(dá)到自己的期望盅粪,舉個(gè)例子:

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)
  }
}

說(shuō)說(shuō)這段代碼不好的地方:

  1. 這個(gè)裝飾器入?yún)懰懒撕瘮?shù)的類型:

    type SumFunc func(int64, int64) int64
    

    也就是說(shuō)票顾,只要換一個(gè)函數(shù)帆调,這個(gè)裝飾器就不能用了番刊,這不符合我們的第2點(diǎn)要求

  2. 這里時(shí)間統(tǒng)計(jì)結(jié)果直接打印到了標(biāo)準(zhǔn)輸出芹务,也就是說(shuō)這個(gè)結(jié)果是不能被原函數(shù)的調(diào)用方去使用的:因?yàn)橹挥械粲梅剑胖肋@個(gè)結(jié)果符不符合預(yù)期熔吗,是花太多時(shí)間了佳晶,還是正常現(xiàn)象中跌。這不符合我們的第3點(diǎn)要求晒他。

怎么解決這兩個(gè)問(wèn)題呢逸贾?

這個(gè)時(shí)候津滞,《重構(gòu)触徐,改善既有代碼的設(shè)計(jì)》告訴我們:Replace Method with Method Obejct——以函數(shù)對(duì)象取代函數(shù)狐赡。他的意思是當(dāng)一個(gè)函數(shù)有比較復(fù)雜的臨時(shí)變量時(shí),我們可以考慮將函數(shù)封裝成一個(gè)類鸟雏。這樣我們的函數(shù)就統(tǒng)一成了 0 個(gè)參數(shù)览祖。(當(dāng)然展蒂,原本就是作為一個(gè) struct 里面的方法的話就適當(dāng)做調(diào)整就好了)

現(xiàn)在锰悼,我們的代碼變成了這樣:

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)
  }
}

這里入?yún)懗墒且粋€(gè) interface ,目的是允許各種函數(shù)對(duì)象入?yún)⒛褪恚恍枰獙?shí)現(xiàn)了 SetCost 和 TimeCost 方法即可

對(duì)并發(fā)編程友好

最后需要考慮的一個(gè)問(wèn)題可柿,很多時(shí)候,一個(gè)類在整個(gè)程序的生命周期是一個(gè)單例营密,這樣在 SetCost 的時(shí)候,就需要考慮并發(fā)寫的問(wèn)題纷捞。這里考慮一下幾種解決方案:

  1. 使用裝飾器配套的時(shí)間統(tǒng)計(jì)存儲(chǔ)對(duì)象主儡,實(shí)現(xiàn)如下:

    
    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. 抽離出存粹的執(zhí)行完就可以銷毀的函數(shù)對(duì)象糜值,每次要操作的時(shí)候都 new 一下

  3. 函數(shù)對(duì)象內(nèi)部對(duì) SetCost 函數(shù)實(shí)現(xiàn)鎖機(jī)制

這三個(gè)方案是按推薦指數(shù)從高到低排序的,因?yàn)槲覀€(gè)人認(rèn)為:資源允許的情況下病往,盡量保持對(duì)象不可變停巷;同時(shí)怎么統(tǒng)計(jì)畔勤、存儲(chǔ)使用時(shí)長(zhǎng)其實(shí)是統(tǒng)計(jì)時(shí)間模塊自己的事情扒磁。

單元測(cè)試

最后補(bǔ)上單元測(cè)試:

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,
      }
    }) 
  }
}

測(cè)試通過(guò)渗磅,驗(yàn)證了時(shí)間統(tǒng)計(jì)是沒(méi)問(wèn)題的始鱼。至此,這個(gè)時(shí)間統(tǒng)計(jì)裝飾器就介紹完了起暮。如果這個(gè)實(shí)現(xiàn)有什么問(wèn)題会烙,或者大家有更好的實(shí)現(xiàn)方式柏腻,歡迎大家批評(píng)指正與提出~

原文地址:https://blog.coordinate35.cn/...
感謝作者:coordinate35
查看原文:Golang 函數(shù)執(zhí)行時(shí)間統(tǒng)計(jì)裝飾器的一個(gè)實(shí)現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末五嫂,一起剝皮案震驚了整個(gè)濱河市沃缘,隨后出現(xiàn)的幾起案子槐臀,更是在濱河造成了極大的恐慌水慨,老刑警劉巖敬扛,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異槽棍,居然都是意外死亡炼七,警方通過(guò)查閱死者的電腦和手機(jī)豌拙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門陕悬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人按傅,你說(shuō)我怎么就攤上這事捉超。” “怎么了唯绍?”我有些...
    開(kāi)封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵拼岳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我况芒,道長(zhǎng)惜纸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任绝骚,我火速辦了婚禮耐版,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粪牲。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布朝氓。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上印蔗,一...
    開(kāi)封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天耙厚,我揣著相機(jī)與錄音,去河邊找鬼泛豪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛价卤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼缆娃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了椭住?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挖息,失蹤者是張志新(化名)和其女友劉穎绪抛,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沮明,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年纺酸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片用含。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖虽惭,靈堂內(nèi)的尸體忽然破棺而出取劫,到底是詐尸還是另有隱情炮捧,我是刑警寧澤扯俱,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布箩艺,位于F島的核電站,受9級(jí)特大地震影響静汤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一贸典、第九天 我趴在偏房一處隱蔽的房頂上張望蔬充。 院中可真熱鬧庸队,春花似錦宾尚、人聲如沸锥忿。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至晃洒,卻和暖如春吃引,著一層夾襖步出監(jiān)牢的瞬間庐氮,已是汗流浹背议泵。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凡蜻,地道東北人忠荞。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像计维,于是被迫代替她去往敵國(guó)和親欢策。 傳聞我的和親對(duì)象是個(gè)殘疾皇子六水,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 這篇文章將會(huì)介紹 etcd 的實(shí)現(xiàn)原理荣茫,其中包括 Raft 協(xié)議啡莉、存儲(chǔ)兩大模塊旨剥,在最后我們也會(huì)簡(jiǎn)單介紹 etcd ...
    小刀愛(ài)編程閱讀 1,917評(píng)論 0 3
  • 本文為《爬著學(xué)Python》系列第四篇文章烟具。從本篇開(kāi)始朝聋,本專欄在順序更新的基礎(chǔ)上,會(huì)有不規(guī)則的更新。 在Pytho...
    SyPy閱讀 2,501評(píng)論 4 11
  • 閉包和裝飾器 1.8 閉包和裝飾器 學(xué)習(xí)目標(biāo) 1. 能夠說(shuō)出閉包的定義形式 2. 能夠說(shuō)出裝飾器的實(shí)現(xiàn)形式 ...
    Cestine閱讀 538評(píng)論 0 0
  • 在學(xué)習(xí)Python的過(guò)程中,我相信有很多人和我一樣蕊蝗,對(duì)Python的裝飾器一直覺(jué)得很困惑仅乓,我也是困惑了好久,并通過(guò)...
    愚灬墨閱讀 460評(píng)論 1 1
  • 2017-03-11作業(yè)匯總 作者:容玲文章:早起蓬戚,從來(lái)拼的不是意志力 作者:瓦力_CC文章:原來(lái)除了堅(jiān)持早起夸楣,還...
    BigQ個(gè)人成長(zhǎng)閱讀 541評(píng)論 0 51