请大家看下面这段代码,看运行结果会出现什么,为什么?
问题
demo
package main import ( "log" "time" ) func main() { start := time.Now() defer func() { log.Printf("匿名函数时间差: %v", time.Since(start)) }() defer log.Printf("时间差: %v", time.Since(start)) time.Sleep(3 * time.Second) log.Printf("函数结束") }
这里不少同学会认为:这题我会,先输出
函数结束
,待整个函数结束后,根据defer后进先出
的顺序依次打印计算的时间差,由于这里睡眠了3秒
,两个时间差应该都是3秒左右
。
所以答案为:
函数结束
时间差:3s
匿名函数时间差:3s
看到这里,我想说同学你的思路和想法是好的,一开始我也和你一样,但是这里的答案是错的,那为什么错呢?总得有个原因吧!很明显匿名函数的时间差符合逻辑是对的,那为什么时间差与预期不符合呢?下面我来进行分析。
运行结果
分析
这里的关键点在于为什么时间差
为0
,也就是说为什么时间差
这个defer
语句没有被time.sleep
所影响?进一步分析,就是查看start
的赋值时机在哪?是在一开始调用defer
就赋值,还是说在函数结束后给defer
中的start
赋值,从而造成结果的不同。
带着这个问题,不妨来debug一下,看一下函数语句的执行顺序。
设置断点
要想看一下start
的赋值时机,设置断点在出现start
变量前面即可。
设置好断点后,开始进行debug
debug查看
step1
一开始,先初始化start
的值
step2
接着,来到第一个defer
+ 匿名函数
,看它有没有进去里面的printf
语句给start
变量进行赋值,step over
直接跳过了匿名函数
的defer
语句,也就是说明并没有给匿名函数
中的defer
语句中的start
赋值
step3
之后,进入defer
语句中,给time.Since
中的start
进行赋值。
这里,会发现问题的关键所在,对比defer
+ 匿名函数
一开始调用(非执行)时,不会对里面的变量(参数)start
进行赋值。
然而普通的defer
+ printf
则在一开始时就会对里面的变量start
赋值,赋值后不会先把time.since
的结果计算出来,会在defer
调用后,也就是光标移动到下一条语句时调用time.sleep
计算时间差
,其结果就是0s
左右,此时不会在调用时输出,待整个函数执行完毕退出后,依次按照defer
顺序输出。
step4
进一步来到了time.sleep
,这也说明step3
确实给defer
语句赋值了,并没有跳过,现在开始休眠3
s, 观察3
s后会光标会去到哪里?
猜想:3s
后会先输出函数结束
,之后函数退出,开始执行defer
语句.
结合匿名函数的时间差为3s
左右,又因为defer
+匿名函数
中的start
还未赋值,会回到开头的defer
+ 匿名函数
进行赋值。
等待3s钟
step5
3s
后,来到了输出函数结束
的语句。
进一步函数退出,也就是整个函数执行完毕!
step6
又回到了刚才的defer
+ 匿名函数
果然与step4
的猜想一致!
关键点来了!如下图:这里会进入defer
+ 匿名函数
, 并给里面的start
变量赋值。
注意,这里传参start
还是一开始的start
! 只不过time.since(start)
的time
增加(休眠)了3s
,这也很好的解释了为什么defer
+ 匿名函数 输出的是3s
左右,而defer
+ printf
语句却是输出0s
左右。
之后匿名函数结束
退出当前的整个函数
最后整个函数结束,输出debug
的结果:这里加了一些debug
调试的时间,以运行结果为准,见下。
运行结果如下:
探讨
在debug
后,我们来探讨一下为什么会出现这样的情况?为什么结果会有所不同?里面的机制是什么?
defer + 输出语句
传值时机:一开始调用defer
时传入
Go 语言中所有的函数调用都是传值的
虽然 defer
是关键字,但是也继承了这个特性。假设我们想要计算 main 函数运行的时间,可能会写出以下的代码:
package main import ( "fmt" "time" ) func main() { start := time.Now() // 这里误以为:startedAt是在time.Sleep之后才会将参数传递给defer所在语句的函数中 defer fmt.Println(time.Since(start)) time.Sleep(3 * time.Second) }
关键点:调用defer
关键字会立刻拷贝函数中引用的外部参数
所以 time.Since(start)
的结果不是在 main 函数退出之前计算的,而是在 defer
关键字调用时赋值计算的,最终导致上述代码输出 0s
。
defer + 匿名函数
传值时机:main函数结束后,执行defer
函数 时传入,传入函数指针
package main import ( "fmt" "time" ) func main() { start := time.Now() // 使用匿名函数,传递的是函数的指针 defer func() { fmt.Println(time.Since(start)) }() time.Sleep(3 * time.Second) }
那为什么使用匿名函数就可以输出3s
呢?关键点:defer
使用匿名函数 , 传递的是函数的指针(函数是一种指针类型),结合刚才的断点分析,不会在一开始调用defer
+ 匿名函数
的时候就直接赋值,而是在后面main
函数退出时,再执行defer
+匿名函数
时再传入函数的指针,并给start
变量赋值。
总结
总而言之:一开始调用defer+输出语句
会进行传值
,会在调用defer
的时候就直接传递值的拷贝(如刚才的defer+输出语句)
,从而计算时间差。
调用defer + 匿名函数
传递的是指针类型(如函数、匿名函数),
会在main
函数退出时,在执行defer
+匿名函数
时再传入函数的指针,并给里面的变量赋值。
归根结底,是传入的类型不同,继而导致传入变量的时机不同,造成输出的结果不同!
扩展
面试官:请你用defer
与time.sleep
写一个计算函数运行时间
的程序
相信看到这里的小伙伴,已经很清楚要写什么代码了!
package main import ( "fmt" "time" ) func main() { start := time.Now() // 使用匿名函数,传递的是函数的指针 defer func() { fmt.Println(time.Since(start)) }() time.Sleep(3 * time.Second) }
到此这篇关于Go defer与time.sleep的使用与区别的文章就介绍到这了,更多相关Go defer与time.sleep内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!