Golang 函数执行时间统计装饰器的一个实现详解

作者:coordinate35 时间:2024-05-09 09:46:22 

背景

最近在搭一个新项目的架子,在生产环境中,为了能实时的监控程序的运行状态,少不了逻辑执行时间长度的统计。时间统计这个功能实现的期望有下面几点:

  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)原则。因此为了实现逻辑的复用,我认为装饰器是比较好的实现方式:将要执行的函数作为参数传入到时间统计函数中。

举个网上看到的例子

实现一个功能,第一反应肯定是查找同行有没有现成的轮子。不过看了下,没有达到自己的期望,举个例子:


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

说说这段代码不好的地方:

这个装饰器入参写死了函数的类型:


type SumFunc func(int64, int64) int64

也就是说,只要换一个函数,这个装饰器就不能用了,这不符合我们的第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 的时候,就需要考虑并发写的问题。这里考虑一下几种解决方案:

使用装饰器配套的时间统计存储对象,实现如下:


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
}

抽离出存粹的执行完就可以销毁的函数对象,每次要操作的时候都 new 一下

函数对象内部对 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://segmentfault.com/a/1190000018538275

标签:Golang,函数,时间统计,装饰器
0
投稿

猜你喜欢

  • 在ASP.NET 2.0中操作数据之二十七:创建自定义排序用户界面

    2023-06-25 21:01:05
  • Pytorch自己加载单通道图片用作数据集训练的实例

    2022-02-06 22:56:03
  • ie7空格的间距要比ie6/firefox/opera的都要大

    2008-05-24 16:54:00
  • python基于BeautifulSoup实现抓取网页指定内容的方法

    2022-11-29 12:11:48
  • ajax完美实现两个网页 分页功能的实例代码

    2023-11-14 12:57:27
  • Go语言中sync.Cond使用详解

    2024-04-25 15:28:23
  • 基于JS实现01支付后的10秒倒计时

    2024-04-10 11:02:30
  • Python实现注册登录功能

    2023-09-07 22:24:50
  • Python2.x中文乱码问题解决方法

    2023-09-20 14:35:35
  • Python 2/3下处理cjk编码的zip文件的方法

    2022-08-05 17:53:40
  • 基于Python实现天天酷跑功能

    2022-09-30 01:18:33
  • Python程序中设置HTTP代理

    2023-01-06 11:32:23
  • numpy.insert用法及内插插0的方法

    2023-03-28 10:06:13
  • 土豆网前端概况

    2008-01-24 12:06:00
  • Python判断是否json是否包含一个key的方法

    2023-02-25 02:58:40
  • Python读取和存储yaml文件的方法

    2023-03-26 10:23:44
  • 讲解无法打开用户默认数据库的解决方法

    2008-12-05 15:55:00
  • JavaScript预解析及相关技巧分析

    2024-04-10 10:57:32
  • Python实现简单飞机大战

    2022-11-11 17:39:11
  • python 实现多进程日志轮转ConcurrentLogHandler

    2021-03-01 02:03:42
  • asp之家 网络编程 m.aspxhome.com