Golang 函数执行时间统计装饰器的一个实现详解
作者:coordinate35 发布时间:2024-05-09 09:46:22
背景
最近在搭一个新项目的架子,在生产环境中,为了能实时的监控程序的运行状态,少不了逻辑执行时间长度的统计。时间统计这个功能实现的期望有下面几点:
实现细节要剥离:时间统计实现的细节不期望在显式的写在主逻辑中。因为主逻辑中的其他逻辑和时间统计的抽象层次不在同一个层级
用于时间统计的代码可复用
统计出来的时间结果是可被处理的。
对并发编程友好
实现思路
统计细节的剥离
最朴素的时间统计的实现,可能是下面这个样子:
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


猜你喜欢
- 一.ASP使用SQL查询数据库方法: 方法1 Set&nbs
- 介绍目前它是Python数值计算中最为重要的基础包,将numpy的数组的对象作为数据交互的通用语,一般我们要充分理解好矩阵计算的原理,这需要
- 昨天还和裕波沟通我没有准备ppt,想以另一种互动的方式来交流。后来想想,每个人都准备了,我不准备也不太好意思,呵呵。今天在webrebuil
- 之前用小程序做项目,因为后台使用的java开发,一切顺利,但切换成django做RESTful API接口时,在登陆注册时一直出现问题,网上
- import "strconv"strconv 包实现了基本数据类型和其字符串表示的相互转换。string与int类型转
- 本文实例讲述了Go语言使用sort包对任意类型元素的集合进行排序的方法。分享给大家供大家参考。具体如下:使用sort包的函数进行排序时,集合
- 背景我打开手机,发现有人在QQ空间里叫嚣。看他得意的样子,显然是在家里呆久了,已经忘了天有多高。预处理设计一个迷宫自动寻路算法并不难,但是对
- 考虑下述Python代码片段。对文件中的数据进行某些操作,然后将结果保存回文件中:with open(filename) as f:&nbs
- 在Perfection kills上看到他去年写的一篇文章,关于HTML优化的,讲的很详细,姑且记录之,尽管里面有些东西并不能在目前的环境里
- Python 直接连接mongodb数据库进行查询操作1、安装所需模块使用到的是pymongo模块,安装方法:pip instal
- 一张表(ColumnTable)的结构如下图所示当前需要实现的功能:通过Number的值为67来获取当前的节点ID、父节点ID递归实现SQL
- Mysql-connector-java驱动版本问题由于我的数据库版本是5.7.28 ,在使用java连接mysql时经常出现版本问题。co
- 在网页中放iframe,如果frameborder=0;就没有边框显示了;但动态创建时,在IE7中就不行了,从网上找到解决的办法,写出来记录
- 一个动态数组 a,如果你已经使用redim 语句给它设定了大小,那么在此之后使用 ubound(a) 就可以得到它的上边界。如果你没有使用
- 0X01函数说明:python range() 函数可创建一个整数列表,一般用在 for 循环中。0X02函数语法:range(start,
- 生成HTML方法主要步骤只有两个:一、获取要生成的html文件的内容二、将获取的html文件内容保存为html文件我在这里主要说明的只是第一
- 如下所示:#coding=utf-8#方式一print('*'*20 + '方式一' + '*
- 一般跟踪训练的ground_truth的数据保存在文本文文件中,故每一行的数据为一张图片的标签数据,这个时候读取每一张图片的标签,具体实现如
- 最近很多小伙伴在尝鲜chatGPT,使用中遇到网站的1020的错误码,博主也遇到了相似的问题,不同的人运行环境不一样,可能解决方案不一样,接
- date() 获取日期,格式:2004-2-28 time() 获取时间,格式:22:24:59 now() 获取日期和时间 格式: 200