最近在搭一个新项目的架子,在生产环境中,为了能实时的监控程序的运行状态,少不了逻辑执行时间长度的统计。时间统计这个功能实现的指望有下面几点:html
最朴素的时间统计的实现,多是下面这个样子:编程
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
这个装饰器入参写死了函数的类型:htm
type SumFunc func(int64, int64) int64
也就是说,只要换一个函数,这个装饰器就不能用了,这不符合咱们的第2点要求
这个时候,《重构,改善既有代码的设计》告诉咱们: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 的时候,就须要考虑并发写的问题。这里考虑一下几种解决方案:
使用装饰器配套的时间统计存储对象,实现以下:
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 }
这三个方案是按推荐指数从高到低排序的,由于我我的认为:资源容许的状况下,尽可能保持对象不可变;同时怎么统计、存储使用时长实际上是统计时间模块本身的事情。
最后补上单元测试:
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, } }) } }
测试经过,验证了时间统计是没问题的。至此,这个时间统计装饰器就介绍完了。若是这个实现有什么问题,或者你们有更好的实现方式,欢迎你们批评指正与提出~