Go语言中文网,致力于每日分享编码、开源等知识,欢迎关注我,会有意想不到的收获!
什么是 defer?
defer 语句的用途是:含有 defer 语句的函式,会在该函式将要返回之前,呼叫另一个函式。这个定义可能看起来很复杂,我们通过一个示例就很容易明白了。示例
上面的程式很简单,就是找出一个给定切片的最大值。largest 函式接收一个 int 型别的切片作为引数,然后打印出该切片中的最大值。largest 函式的第一行的语句为 defer finished()。这表示在 finished() 函式将要返回之前,会呼叫 finished() 函式。执行该程式,你会看到有如下输出:
largest 函式开始执行后,会打印上面的两行输出。而就在 largest 将要返回的时候,又呼叫了我们的延迟函式(Deferred Function),打印出 Finished finding largest 的文字。
延迟方法
defer 不仅限于函式的呼叫,呼叫方法也是合法的。我们写一个小程式来测试吧。在上面的例子中,我们在第 22 行延迟了一个方法呼叫。而其他的程式码很直观,这里不再解释。该程式输出:
Welcome John Smith
实参取值(Arguments Evaluation)
在 Go 语言中,并非在呼叫延迟函式的时候才确定实参,而是当执行 defer 语句的时候,就会对延迟函式的实参进行求值。通过一个例子就能够理解了。
在上面的程式里的第 11 行,a 的初始值为 5。在第 12 行执行 defer 语句的时候,由于 a 等于 5,因此延迟函式 printA 的实参也等于 5。接着我们在第 13 行将 a 的值修改为 10。下一行会打印出 a 的值。该程式输出:
从上面的输出,我们可以看出,在呼叫了 defer 语句后,虽然我们将 a 修改为 10,但呼叫延迟函式 printA(a)后,仍然打印的是 5。
defer 栈
当一个函式内多次呼叫 defer 时,Go 会把 defer 呼叫放入到一个栈中,随后按照后进先出(Last In First Out, LIFO)的顺序执行。我们下面编写一个小程式,使用 defer 栈,将一个字串逆序打印。
在上述程式中的第 11 行,for range 循环会遍历一个字串,并在第 12 行呼叫了 defer fmt.Printf(%c, v)。这些延迟呼叫会新增到一个栈中,按照后进先出的顺序执行,因此,该字串会逆序打印出来。该程式会输出:
Orignal String: Naveen
Reversed String: neevaN
defer 的实际应用
目前为止,我们看到的程式码示例,都没有体现出 defer 的实际用途。本节我们会看看 defer 的实际应用。当一个函式应该在与当前程式码流(Code Flow)无关的环境下呼叫时,可以使用 defer。我们通过一个用到了 WaitGroup 程式码示例来理解这句话的含义。我们首先会写一个没有使用 defer 的程式,然后我们会用 defer 来修改,看到 defer 带来的好处。
在上面的程式里,我们在第 8 行建立了 rect 结构体,并在第 13 行建立了 rect 的方法 area,计算出矩形的面积。area 检查了矩形的长宽是否小于零。如果矩形的长宽小于零,它会打印出对应的提示资讯,而如果大于零,它会打印出矩形的面积。
main 函式建立了 3 个 rect 型别的变数:r1、r2 和 r3。在第 34 行,我们把这 3 个变数新增到了 rects 切片里。该切片接着使用 for range 循环遍历,把 area 方法作为一个并发的 Go 协程进行呼叫(第 37 行)。我们用 WaitGroup wg 来确保 main 函式在其他协程执行完毕之后,才会结束执行。WaitGroup 作为引数传递给 area 方法后,在第 16 行、第 21 行和第 26 行通知 main 函式,表示现在协程已经完成所有任务。如果你仔细观察,会发现 wg.Done() 只在 area 函式返回的时候才会呼叫。wg.Done() 应该在 area将要返回之前呼叫,并且与程式码流的路径(Path)无关,因此我们可以只调用一次 defer,来有效地替换掉 wg.Done() 的多次呼叫。
我们来用 defer 来重写上面的程式码。
在下面的程式码中,我们移除了原先程式中的 3 个 wg.Done 的呼叫,而是用一个单独的 defer wg.Done() 来取代它(第 14 行)。这使得我们的程式码更加简洁易懂。
该程式会输出:
在上面的程式中,使用 defer 还有一个好处。假设我们使用 if 条件语句,又给 area 方法添加了一条返回路径(Return Path)。如果没有使用 defer 来呼叫 wg.Done(),我们就得很小心了,确保在这条新添的返回路径里呼叫了 wg.Done()。由于现在我们延迟呼叫了 wg.Done(),因此无需再为这条新的返回路径新增 wg.Done() 了。
本教程到此结束。祝你愉快。
上一教程 - “GCTT 出品”Go 系列教程——28. 多型
下一教程 - 错误处理
历史文章:
“GCTT 出品”Go 系列教程——1. 介绍与安装
“GCTT 出品”Go 系列教程——2. Hello World
“GCTT 出品”Go 系列教程——3. 变数
“GCTT 出品”Go 系列教程——4. 型别
“GCTT 出品”Go 系列教程——5. 常量
“GCTT 出品”Go 系列教程——6. 函式(Function)
“GCTT 出品”Go 系列教程——7. 包
Go 系列教程——8. if-else 语句
“GCTT 出品”Go 系列教程——9. 循环
“GCTT 出品”Go 系列教程——10. switch 语句
“GCTT 出品”Go 系列教程——11. 阵列和切片
“GCTT 出品”Go 系列教程——12. 可变引数函式
“GCTT 出品”Go 系列教程——13. Maps
“GCTT 出品”Go 系列教程——14. 字串
“GCTT 出品”Go 系列教程——15. 指标
“GCTT 出品”Go 系列教程——16. 结构体,这一篇就够
“GCTT 出品”Go 系列教程——17. 超全的方法教程
“GCTT 出品”Go 系列教程——18. 界面(一)
“GCTT 出品”Go 系列教程——19. 界面(二)
“GCTT 出品”Go 系列教程——20. 并发入门
“GCTT 出品”Go 系列教程——21. Go 协程
“GCTT 出品”Go 系列教程——22. 通道(channel)
“GCTT 出品”Go 系列教程——23. 缓冲通道和工作池
“GCTT 出品”Go 系列教程——24. Select
“GCTT 出品”Go 系列教程——25. Mutex
“GCTT 出品”Go 系列教程——26. 结构体取代类
“GCTT 出品”Go 系列教程——27. 组合取代继承
“GCTT 出品”Go 系列教程——28. 多型