Go:测量函数执行时间的方法

汪明军_3145 · · 1086 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

【译文】原文地址
对函数进行分析是很重要的。了解函数的执行时间可以发现代码中隐藏的瓶颈,在生产环境中发布特性之前提供清晰的延时估算,能够为查找代码中低效算法提供线索。然而,测试函数的执行时间不是函数的核心业务逻辑,因此测量函数执行时间的代码不应该对业务代码有侵入。

低级实现方式

假设我们有一个函数做一些耗时的事。比如,每间隔100ms打印1到5整数。代码如下:

func expensivePrint() {
   for i := 1; i <= 5; i++ {
      fmt.Printf("Current number is %d \n", i)
      time.Sleep(100 * time.Millisecond)
   }
}

然后main函数调用上面的函数,完成执行。

func main() {
   expensivePrint()
   fmt.Println("Finished executing main")
}

执行结果:

Current number is 1 
Current number is 2 
Current number is 3 
Current number is 4 
Current number is 5 
Finished executing main

现在,出于监控目的,我们想对expensivePrint函数执行时间进行测量。一种简单的方法是在函数开始处记录开始时间,并在函数结束时检查与当前时间的差异。

func expensivePrint() {
   start := time.Now()
   for i := 1; i <= 5; i++ {
      fmt.Printf("Current number is %d \n", i)
      time.Sleep(100 * time.Millisecond)
   }
   fmt.Printf("Time taken by expensivePrint function is %v \n", time.Since(start))
}

执行main函数结果为:

Current number is 1 
Current number is 2 
Current number is 3 
Current number is 4 
Current number is 5 
Time taken by expensivePrint function is 511.532405ms 
Finished executing main

正如我们所看到的,这与预期的一样有效。然而,这种方法存在一些问题。

  • 问题一:代码的侵入性
    这种方法的一个问题是,代码本质上是非常具有侵入性的。您必须记住在每个函数开始时计算开始时间,并在每个函数结束时测量时间差。
  • 问题二:不能统一日志记录规则
    这种方法的另一个问题是,即使团队中的开发人员都记得添加测量时间的代码,但在函数最后一行记录时间差是的记录格式不能保证每个人都统一,这对后期查找对应的日志造成困难。例如,一些开发人员可能用不同的方式写日志:
fmt.Printf("A different sentence to measure execution of %v \n", time.Since(start))

这使得您越来越难以发现代码中的问题。您需要一种方法,使所有的时间日志记录语句看起来相同。

  • 问题三:不支持修改
    如果更新写入日志的方式,需要对记录执行时间的每个函数进行代码更改。例如:如果您已经从使用fmt改为使用log,那么所有使用上述方式的函数都需要进行代码更新。

高级实现方式

为了解决以上所有问题,让我们看看高级的实现。首先,为了解决后两个问题,我们有必要将时间测量代码放到一个单独的函数中,并在需要的地方调用该函数。为了做到这一点,我们创建一个函数,可接收函数执行开始时间参数和被测量函数名称,并打印日志:

func printTimeMeasurement(funcName string, start time.Time)  {
      fmt.Printf("Time taken by %s function is %v \n", funcName, time.Since(start))
}

调用这个函数替换我们对fmt包的直接调用,使我们的代码具有同质性和可维护性。但是,它仍然是侵入性的,因为它需要在函数开始时记录开始时间。我们将使用Golang提供的defer语句来解决这个问题。根据Golang官方博客:

defer语句将函数调用放到一个栈内。压入栈的函数将在所在函数返回之执行。

我们将printTimeMeasurement函数改为,将实际打印执行时间的日志功能作为函数返回。

func printTimeMeasurement(funcName string, start time.Time) func() {
   return func() {
      fmt.Printf("Time taken by %s function is %v \n", funcName, time.Since(start))
   }
}

现在,让我们编写一个函数measureTime来跟踪开始时间,并返回一个调用:

func measureTime(funcName string) func() {
   start := time.Now()
   return printTimeMeasurement(funcName, start)
}

接下来,将原先的expensivePrint函数,直接使用defer语句调用上面的 measureTime,传入函数名即可:

func expensivePrint() {
   defer measureTime("expensivePrint")()
   for i := 1; i <= 5; i++ {
      fmt.Printf("Current number is %d \n", i)
      time.Sleep(100 * time.Millisecond)
   }
}

输出为:

Current number is 1 
Current number is 2 
Current number is 3 
Current number is 4 
Current number is 5 
Time taken by expensivePrint function is 514.630708ms 
Finished executing main

简化代码

measureTime中的printTimeMeasurement函数可以使用匿名函数代替,简化为如下:

func measureTime(funcName string) func() {
   start := time.Now()
   return func() {
      fmt.Printf("Time taken by %s function is %v \n", funcName, time.Since(start))
   }
}

匿名函数可以使用start和funcName变量,因此不需要这些参数作为函数参数。下面是完整代码:

package main

import (
    "fmt"
    "time"
)

func measureTime(funcName string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("Time taken by %s function is %v \n", funcName, time.Since(start))
    }
}

func expensivePrint() {
    defer measureTime("expensivePrint")()
    for i := 1; i <= 5; i++ {
        fmt.Printf("Current number is %d \n", i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    expensivePrint()
}

代码非常简单,现在只需向代码库中的所有函数添加一个defer调用,就可以测量它们的运行时间。对度量时间函数所做的任何更改,都会在整个代码库中反映出来,从而确保代码的可维护性。

使用惯用的Golang模式,如延迟和匿名函数,可以帮助我们编写简单、高效的代码。这是一个例子。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:汪明军_3145

查看原文:Go:测量函数执行时间的方法

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1086 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传