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

背景

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

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

测试通过,验证了时间统计是没问题的。至此,这个时间统计装饰器就介绍完了。如果这个实现有什么问题,或者大家有更好的实现方式,欢迎大家批评指正与提出~

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 图解Golang的GC垃圾回收算法

    虽然Golang的GC自打一开始,就被人所诟病,但是经过这么多年的发展,Golang的GC已经改善了非常多,变得非常优秀了. 以下是Golang GC算法的里程碑: v1.1 STW v1.3 Mark STW, Sweep 并行 v1.5 三色标记法 v1.8 hybrid write barrier 经典的GC算法有三种: 引用计数(reference counting) . 标记-清扫(mark & sweep) . 复制收集(Copy and Collection) . Golang的G

  • golang time包的用法详解

    在我们编程过程中,经常会用到与时间相关的各种务需求,下面来介绍 golang 中有关时间的一些基本用法,我们从 time 的几种 type 来开始介绍. 时间可分为时间点与时间段,golang 也不例外,提供了以下两种基础类型 - 时间点(Time) - 时间段(Duration) 除此之外 golang 也提供了以下类型,做一些特定的业务 - 时区(Location) - Ticker - Timer(定时器) 我们将按以上顺序来介绍 time 包的使用. 时间点(Time) 我们使用的所有与

  • Golang捕获panic堆栈信息的讲解

    golang当中panic的时候如果启动的goroutine比较多,刷的信息满屏都是,在终端工具上因为刷的信息太多,找不到前边的信息,因此很有必要程序自己捕获panic,并且将错误信息输出到文件当中,以便定位排查问题. Golang捕获panic堆栈信息 func PanicTrace(kb int) []byte { s := []byte("/src/runtime/panic.go") e := []byte("\ngoroutine ") line := [

  • golang中range在slice和map遍历中的注意事项

    golang中range在slice和map遍历中的注意事项 package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3} myMap := make(map[int]*int) for _,v :=range slice{ if v==1 { v=100 } } for k,v :=range slice{ fmt.Println("k:",k,"v:",v) }

  • golang读取文件的常用方法总结

    使用go语言读取文件的各种方式整理. 一次性加载到内存中 // * 整个文件读到内存,适用于文件较小的情况 //每次读取固定字节 //问题容易出现乱码,因为中文和中文符号不占一个字符 func readAllIntoMemory(filename string) (content []byte, err error) { fp, err := os.Open(filename) // 获取文件指针 if err != nil { return nil, err } defer fp.Close(

  • 简单谈谈Golang中的字符串与字节数组

    前言 字符串是 Go 语言中最常用的基础数据类型之一,虽然字符串往往都被看做是一个整体,但是实际上字符串是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组,Go 语言中另外一个与字符串关系非常密切的类型就是字节(Byte)了,相信各位读者也都非常了解,这里也就不展开介绍. 我们在这一节中就会详细介绍这两种基本类型的实现原理以及它们的转换关系,但是这里还是会将介绍的重点主要放在字符串上,因为这是我们接触最多的一种基本类型并且后者就是一个简单的 uint8 类型,所以会给予 string

  • Golang 使用接口实现泛型的方法示例

    在C/C++中我们可以使用泛型的方法使代码得以重复使用,最常见例如stl functions:vector<int> vint or vector<float> vfloat等.这篇文章将使用interface{...}接口使Golang实现泛型. interface{...}是实现泛型的基础.如一个数组元素类型是interface{...}的话,那么实现了该接口的实体都可以被放置入数组中.注意其中并不一定必须是空接口(简单类型我们可以通过把他转化为自定义类型后实现接口).为什么i

  • golang 并发安全Map以及分段锁的实现方法

    涉及概念 并发安全Map 分段锁 sync.Map CAS ( Compare And Swap ) 双检查 分断锁 type SimpleCache struct { mu sync.RWMutex items map[interface{}]*simpleItem } 在日常开发中, 上述这种数据结构肯定不少见,因为golang的原生map是非并发安全的,所以为了保证map的并发安全,最简单的方式就是给map加锁. 之前使用过两个本地内存缓存的开源库, gcache, cache2go,其中

  • Golang 日期/时间包的使用详解

    golang 的日期时间包:time 的使用方式. time package 包含了 time.Time 时间对象 及 构建此时间对象的一些方法(time.Unix(), time.Parse()) golang 可精确到 nanosecond,故相应的函数返回值或参数都已纳秒为单位,我们可以借助time.ParseDuration(durationString string)友好的生成纳秒度量的时间跨度值 golang 的时间格式串Layout固定为 2006-01-02 15:04:05 g

  • golang时间、时区、格式的使用方法

    前几天,因为需要实现海外服务端定时停机,涉及到时区的概念.网上搜索了一下,大部分都是谈time.Format中的Layout,非常不成体系,这里就简单总结一下其中的时间初始化.时区转化及格式转换. 开发中,我们对时间的使用是比较多的,其应用场景,按照使用概率,从大到小,通常是: 获取当前或数据库中存储的时间 比较两个时间点的先后 显示打印时间 时区转换 对应到go,也就是几个基本定义: 时间点与时间段:Time,Duration.好比MVC中的M. 时 区:Location,在时间转换上,好比是

随机推荐