defer关键字

defer关键字的作用是当外围函数返回之后才执行被推迟的函数。在文件输入输出操作中经常可以见到defer关键字,因为它使您不必记住何时关闭已打开的文件:defer关键字调用文件关闭函数关闭已打开的文件时,可以紧靠着文件打开函数之后。在第八章“告诉Unix系统要做什么”中,会介绍如何在文件相关操作中使用defer关键字,本节将介绍defer的其他用法。您还将在panic()recover()两个Go内置函数中看到defer操作。

首先记住一点重要的原则:defer函数在外围函数返回之后,以后进先出(LIFO)的原则执行。简单点说,在一个外围函数中有3个defer函数:f1()最先出现,然后f2(),最后f3(),当外围函数执行返回之后,f3()最先被执行,接着是f2(),最后是f1()

如果你感觉这个定义不是很清晰,查看并执行defer.go源码,这应该会让你理解的更清楚些。下面分三部分呈现这段源码。

源码第一部分:

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func d1() {
  6. for i := 3; i > 0; i-- {
  7. defer fmt.Print(i, " ")
  8. }
  9. }

上面的Go代码实现了一个名为d1()函数,函数里面有一个for循环和一个defer语句,这个defer语句将会执行三次。

第二部分源码:

  1. func d2() {
  2. for i := 3; i > 0; i-- {
  3. defer func() {
  4. fmt.Print(i, " ")
  5. }()
  6. }
  7. fmt.Println()
  8. }

这部分代码实现了d2()函数,它也包含了一个for循环和一个defer语句,这个defer语句也会执行三次,但是这个defer执行的是匿名函数,并且匿名函数没有带参数。

最后一部分源码:

  1. func d3() {
  2. for i := 3; i > 0; i-- {
  3. defer func(n int) {
  4. fmt.Print(n, " ")
  5. }(i)
  6. }
  7. }
  8. func main() {
  9. d1()
  10. d2()
  11. fmt.Println()
  12. d3()
  13. fmt.Println()
  14. }

这部分代码,定义了d3()函数,它包含了for循环defer语句,这个defer执行带参数的匿名函数,其中参数n使用的是循环中变量i的值。main()函数调用这三个函数。

执行源码会得到如下输出:

  1. $ go run defer.go
  2. 1 2 3
  3. 0 0 0
  4. 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的最好的方式就是第三个函数展示的方法,这是因为您故意以易于理解的方式在匿名函数中传递所需的变量。