背景
最近在搭一個新項目的架子,在生產(chǎn)環(huán)境中,為了能實時的監(jiān)控程序的運行狀態(tài),少不了邏輯執(zhí)行時間長度的統(tǒng)計。時間統(tǒng)計這個功能實現(xiàn)的期望有下面幾點:
- 實現(xiàn)細節(jié)要剝離:時間統(tǒng)計實現(xiàn)的細節(jié)不期望在顯式的寫在主邏輯中。因為主邏輯中的其他邏輯和時間統(tǒng)計的抽象層次不在同一個層級
- 用于時間統(tǒng)計的代碼可復用
- 統(tǒng)計出來的時間結(jié)果是可被處理的。
- 對并發(fā)編程友好
實現(xiàn)思路
統(tǒng)計細節(jié)的剝離
最樸素的時間統(tǒng)計的實現(xiàn),可能是下面這個樣子:
func f() {
startTime := time.Now()
logicStepOne()
logicStepTwo()
endTime := time.Now()
timeDiff := timeDiff(startTime, endTime)
log.Info("time diff: %s", timeDiff)
}
《代碼整潔之道》告訴我們:一個函數(shù)里面的所有函數(shù)調(diào)用都應該處于同一個抽象層級。
在這里時間開始、結(jié)束的獲取,使用時間的求差,屬于時間統(tǒng)計的細節(jié),首先他不屬于主流程必要的一步,其次他們使用的函數(shù) time.Now() 和 logicStepOne, logicStepTwo 并不在同一個抽象層級。
因此比較好的做法應該是把時間統(tǒng)計放在函數(shù) f 的上層,比如:
func doFWithTimeRecord() {
startTime: = time.Now()
f()
endTime := Time.Now()
timeDiff := timeDIff(startTime, endTime)
log.Info("time diff: %s", timeDiff)
}
時間統(tǒng)計代碼可復用統(tǒng)計結(jié)果可被處理不影響原函數(shù)的使用方式
我們雖然達成了函數(shù)內(nèi)抽象層級相同的目標,但是大家肯定也能感受到:這個函數(shù)并不好用。
原因在于,我們把要調(diào)用的函數(shù) f 寫死在了 doFWithTimeRecord 函數(shù)中。這意味著,每一個要統(tǒng)計時間的函數(shù),我都需要實現(xiàn)一個 doXXWithTimeRecord, 而這些函數(shù)里面的邏輯是相同的,這就違反了我們 DRY(Don't Repeat Yourself)原則。因此為了實現(xiàn)邏輯的復用,我認為裝飾器是比較好的實現(xiàn)方式:將要執(zhí)行的函數(shù)作為參數(shù)傳入到時間統(tǒng)計函數(shù)中。
舉個網(wǎng)上看到的例子
實現(xiàn)一個功能,第一反應肯定是查找同行有沒有現(xiàn)成的輪子。不過看了下,沒有達到自己的期望,舉個例子:
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)
}
}
說說這段代碼不好的地方:
這個裝飾器入?yún)懰懒撕瘮?shù)的類型:
type SumFunc func(int64, int64) int64
也就是說,只要換一個函數(shù),這個裝飾器就不能用了,這不符合我們的第2點要求
這里時間統(tǒng)計結(jié)果直接打印到了標準輸出,也就是說這個結(jié)果是不能被原函數(shù)的調(diào)用方去使用的:因為只有掉用方,才知道這個結(jié)果符不符合預期,是花太多時間了,還是正?,F(xiàn)象。這不符合我們的第3點要求。
怎么解決這兩個問題呢?
這個時候,《重構(gòu),改善既有代碼的設(shè)計》告訴我們:Replace Method with Method Obejct——以函數(shù)對象取代函數(shù)。他的意思是當一個函數(shù)有比較復雜的臨時變量時,我們可以考慮將函數(shù)封裝成一個類。這樣我們的函數(shù)就統(tǒng)一成了 0 個參數(shù)。(當然,原本就是作為一個 struct 里面的方法的話就適當做調(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)懗墒且粋€ interface ,目的是允許各種函數(shù)對象入?yún)ⅲ恍枰獙崿F(xiàn)了 SetCost 和 TimeCost 方法即可
對并發(fā)編程友好
最后需要考慮的一個問題,很多時候,一個類在整個程序的生命周期是一個單例,這樣在 SetCost 的時候,就需要考慮并發(fā)寫的問題。這里考慮一下幾種解決方案:
使用裝飾器配套的時間統(tǒng)計存儲對象,實現(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í)行完就可以銷毀的函數(shù)對象,每次要操作的時候都 new 一下
函數(shù)對象內(nèi)部對 SetCost 函數(shù)實現(xiàn)鎖機制
這三個方案是按推薦指數(shù)從高到低排序的,因為我個人認為:資源允許的情況下,盡量保持對象不可變;同時怎么統(tǒng)計、存儲使用時長其實是統(tǒng)計時間模塊自己的事情。
單元測試
最后補上單元測試:
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,
}
})
}
}
測試通過,驗證了時間統(tǒng)計是沒問題的。至此,這個時間統(tǒng)計裝飾器就介紹完了。如果這個實現(xiàn)有什么問題,或者大家有更好的實現(xiàn)方式,歡迎大家批評指正與提出~
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- Golang記錄、計算函數(shù)執(zhí)行耗時、運行時間的一個簡單方法
- 通過匯編看golang函數(shù)的多返回值問題
- Golang學習筆記之延遲函數(shù)(defer)的使用小結(jié)
- Golang中的自定義函數(shù)詳解
- golang 實現(xiàn)每隔幾分鐘執(zhí)行一個函數(shù)