Golang 函數(shù)執(zhí)行時(shí)間統(tǒng)計(jì)裝飾器的一個(gè)實(shí)現(xiàn)詳解
背景
最近在搭一個(gè)新項(xiàng)目的架子,在生產(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):
- 實(shí)現(xiàn)細(xì)節(jié)要?jiǎng)冸x:時(shí)間統(tǒng)計(jì)實(shí)現(xiàn)的細(xì)節(jié)不期望在顯式的寫(xiě)在主邏輯中。因?yàn)橹鬟壿嬛械钠渌壿嫼蜁r(shí)間統(tǒng)計(jì)的抽象層次不在同一個(gè)層級(jí)
- 用于時(shí)間統(tǒng)計(jì)的代碼可復(fù)用
- 統(tǒng)計(jì)出來(lái)的時(shí)間結(jié)果是可被處理的。
- 對(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 寫(xiě)死在了 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ō)這段代碼不好的地方:
這個(gè)裝飾器入?yún)?xiě)死了函數(shù)的類(lèi)型:
type SumFunc func(int64, int64) int64
也就是說(shuō),只要換一個(gè)函數(shù),這個(gè)裝飾器就不能用了,這不符合我們的第2點(diǎn)要求
這里時(shí)間統(tǒng)計(jì)結(jié)果直接打印到了標(biāo)準(zhǔn)輸出,也就是說(shuō)這個(gè)結(jié)果是不能被原函數(shù)的調(diào)用方去使用的:因?yàn)橹挥械粲梅?,才知道這個(gè)結(jié)果符不符合預(yù)期,是花太多時(shí)間了,還是正?,F(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è)類(lèi)。這樣我們的函數(shù)就統(tǒng)一成了 0 個(gè)參數(shù)。(當(dāng)然,原本就是作為一個(gè) struct 里面的方法的話(huà)就適當(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)?xiě)成是一個(gè) interface ,目的是允許各種函數(shù)對(duì)象入?yún)?,只需要?shí)現(xiàn)了 SetCost 和 TimeCost 方法即可
對(duì)并發(fā)編程友好
最后需要考慮的一個(gè)問(wèn)題,很多時(shí)候,一個(gè)類(lèi)在整個(gè)程序的生命周期是一個(gè)單例,這樣在 SetCost 的時(shí)候,就需要考慮并發(fā)寫(xiě)的問(wèn)題。這里考慮一下幾種解決方案:
使用裝飾器配套的時(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 }
抽離出存粹的執(zhí)行完就可以銷(xiāo)毀的函數(shù)對(duì)象,每次要操作的時(shí)候都 new 一下
函數(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)指正與提出~
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Go語(yǔ)言Sync.Pool為何不加鎖也能夠?qū)崿F(xiàn)線(xiàn)程安全
在這篇文章中,我們將剖析sync.Pool內(nèi)部實(shí)現(xiàn)中,介紹了sync.Pool比較巧妙的內(nèi)部設(shè)計(jì)思路以及其實(shí)現(xiàn)方式。在這個(gè)過(guò)程中,也間接介紹了為何不加鎖也能夠?qū)崿F(xiàn)線(xiàn)程安全,感興趣的可以學(xué)習(xí)一下2023-04-04Golang?Compare?And?Swap算法詳細(xì)介紹
CAS算法是一種有名的無(wú)鎖算法。無(wú)鎖編程,即不使用鎖的情況下實(shí)現(xiàn)多線(xiàn)程之間的變量同步,也就是在沒(méi)有線(xiàn)程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步Non-blocking?Synchronization2022-10-10Golang實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能
這篇文章主要為大家詳細(xì)介紹了Golang實(shí)現(xiàn)斷點(diǎn)續(xù)傳、復(fù)制文件功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07Go語(yǔ)言實(shí)現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)二維數(shù)組的2種遍歷方式以及案例詳解,圖文代碼聲情并茂,有感興趣的可以學(xué)習(xí)下2021-03-03