defer关键字
defer关键字的作用是当外围函数返回之后才执行被推迟的函数。在文件输入输出操作中经常可以见到defer关键字,因为它使您不必记住何时关闭已打开的文件:defer关键字调用文件关闭函数关闭已打开的文件时,可以紧靠着文件打开函数之后。在第八章“告诉Unix系统要做什么”中,会介绍如何在文件相关操作中使用defer关键字,本节将介绍defer的其他用法。您还将在panic()
和recover()
两个Go内置函数中看到defer操作。
首先记住一点重要的原则:defer函数在外围函数返回之后,以后进先出(LIFO)的原则执行。简单点说,在一个外围函数中有3个defer函数:f1()
最先出现,然后f2()
,最后f3()
,当外围函数执行返回之后,f3()
最先被执行,接着是f2()
,最后是f1()
。
如果你感觉这个定义不是很清晰,查看并执行defer.go
源码,这应该会让你理解的更清楚些。下面分三部分呈现这段源码。
源码第一部分:
package main
import (
"fmt"
)
func d1() {
for i := 3; i > 0; i-- {
defer fmt.Print(i, " ")
}
}
上面的Go代码实现了一个名为d1()
函数,函数里面有一个for
循环和一个defer
语句,这个defer
语句将会执行三次。
第二部分源码:
func d2() {
for i := 3; i > 0; i-- {
defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}
这部分代码实现了d2()
函数,它也包含了一个for
循环和一个defer
语句,这个defer
语句也会执行三次,但是这个defer
执行的是匿名函数,并且匿名函数没有带参数。
最后一部分源码:
func d3() {
for i := 3; i > 0; i-- {
defer func(n int) {
fmt.Print(n, " ")
}(i)
}
}
func main() {
d1()
fmt.Println()
d2()
fmt.Println()
d3()
fmt.Println()
}
这部分代码,定义了d3()
函数,它包含了for
循环defer
语句,这个defer
执行带参数的匿名函数,其中参数n使用的是循环中变量i的值。main()
函数调用这三个函数。
执行源码会得到如下输出:
$ go run defer.go
1 2 3
0 0 0
1 2 3
您很可能会发现生成的输出很复杂且难以理解。 这表明,如果您的代码不清晰或不明确,执行defer
操作可能会产生非常棘手的结果。
让我们分析一下上面的结果,更进一步了解如果不密切关注自己的代码,defer
将会变得多么棘手。第一行是d1()
函数输出的(1 2 3),变量i的值在这个函数中顺序是3 、2和1 ,在d1()
中延迟的函数是fmt.Print()
。 因此,当d1()
函数即将返回时,您将以相反的顺序获取for循环中变量i的三个值,因为被延迟的函数以LIFO
顺序执行。
第二行是d2()
函数的输出,很奇怪的是我们得到了单个0,而不是想象中的1、2和3 ,原因很简单,在循环结束后,i的值为0,因为是0值使循环终止。但是,这里棘手的部分是在循环结束后会执行被延迟的匿名函数。因为它没有参数,这意味着,将值为0的i进行三次打印输出! 在您的项目中,这种令人困惑的代码可能导致令人讨厌的错误,所以尽量避免它!
第三行是d3()
函数的输出,因为匿名函数的参数的存在,每次匿名函数被推迟执行的时候,它都会获取并使用变量i当前的值,所以每次执行匿名函数都有不同的值输出。
到这里,你应该清楚了,使用defer
的最好的方式就是第三个函数展示的方法,这是因为您故意以易于理解的方式在匿名函数中传递所需的变量。