4.2 defer语句

Go语言拥有一些特有的流程控制语句。其中最常用的就是defer语句。defer语句被用于预定对一个函数的调用。我们把这类被defer语句调用的函数称为延迟函数。注意,defer语句只能出现在函数或方法的内部。

一条defer语句总是以关键字defer开始。在defer的右边还必会有一条表达式语句,且它们之间要以空格“ ”分隔,就像这样:

  1. defer fmt.Println("The finishing touches.")

这里的表达式语句必须代表一个函数或方法的调用。注意,既然是表达式语句,那么一些调用表达式就是不被允许出现在这里的。比如,针对各种内建函数的那些调用表达式。因为它们不能被称为表达式语句。另外,在这个位置上出现的表达式语句是不能被圆括号括起来的。

有意思的是,defer语句的执行时机总是在直接包含它的那个函数(以下简称外围函数)把流程控制权交还给它的调用方的前一刻,无论defer语句出现在外围函数的函数体中的哪一个位置上。具体分为下面几种情况。

  • 当外围函数的函数体中的相应语句全部被正常执行完毕的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地结束执行。

  • 当外围函数的函数体中的return语句被执行的时候,只有在该函数中的所有defer语句都被执行完毕之后该函数才会真正地返回。

  • 当在外围函数中有运行时恐慌发生的时候,只有在该函数中的所有defer语句都被执行完毕之后该运行时恐慌才会真正地被扩散至该函数的调用方。

总之,外围函数的执行的结束会由于其中的defer语句的执行而被推迟。例如:

  1. func isPositiveEvenNumber(number int) (result bool) {
  2. defer fmt.Println("done.")
  3. if number < 0 {
  4. panic(errors.New("The number is a negative number!"))
  5. }
  6. if number%2 == 0 {
  7. return true
  8. }
  9. return
  10. }

在这个示例中,无论参数number是怎样的值,以及该函数的执行会以怎样的方式结束,在该函数的调用方重获流程控制权之前标准输出上都一定会出现done

正因为defer语句有着这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。使用defer语句的优势有两个:一、收尾任务总会被执行,我们不会再因粗心大意而造成资源的浪费;二、我们可以把它们放到外围函数的函数体中的任何地方(一般是函数体开始处或紧跟在申请资源的语句的后面),而不是只能放在函数体的最后。这使得代码逻辑变得更加清晰,并且收尾任务是否被合理的指定也变得一目了然。

defer语句中,我们调用的函数不但可以是已声明的命名函数,还可以是临时编写的匿名函数,就像这样:

  1. defer func() {
  2. fmt.Println("The finishing touches.")
  3. }()

注意,一个针对匿名函数的调用表达式是由一个函数字面量和一个代表了调用操作的一对圆括号组成的。一些刚刚学会编写defer语句的编程者常常会忘记添加后面的那对圆括号。

我们在这里选择匿名函数的好处是可以使该函数的收尾任务的内容更加直观。不过,我们也可以把比较通用的收尾任务单独放在一个命名函数中,然后再将其添加到需要它的defer语句中。无论在defer关键字右边的是命名函数还是匿名函数,我们都可以称之为延迟函数。因为它总是会被延迟到外围函数执行结束前一刻才被真正地调用。

每当defer语句被执行的时候,传递给延迟函数的参数都会以通常的方式被求值。请看下面的示例:

  1. func begin(funcName string) string {
  2. fmt.Printf("Enter function %s.\n", funcName)
  3. return funcName
  4. }
  5. func end(funcName string) string {
  6. fmt.Printf("Exit function %s.\n", funcName)
  7. return funcName
  8. }
  9. func record() {
  10. defer end(begin("record"))
  11. fmt.Println("In function record.")
  12. }

在我们对函数record进行调用之后,标准输出上会打印出如下内容:

  1. Enter function record.
  2. In function record.
  3. Exit function record.

在这个示例中,调用表达式begin("record")是作为record函数的参数出现的。它会在defer语句被执行的时候被求值。也就是说,在record函数的函数体被执行之初,begin函数就被调用了。然而,end函数却是在外围函数record执行结束的前一刻被调用的。

这样做除了可以避免参数值在延迟函数被真正调用之前再次发生改变而给该函数的执行造成影响之外,还是出于同一条defer语句可能会被多次执行的考虑。请看下面的示例代码:

  1. func printNumbers() {
  2. for i := 0; i < 5; i++ {
  3. defer fmt.Printf("%d ", i)
  4. }
  5. }

在函数printNumbers真正执行结束之前,标准输出上会打印出这样的内容:4 3 2 1 0。这里有两个细节需要特别说明。

第一个细节,在for语句的每次迭代的过程中都会执行一次其中的defer语句。在第一次迭代中,针对延迟函数的调用表达式最终会是fmt.Printf("%d ", 0)。这是由于在defer语句被执行的时候,参数i先被求值为了0,随后这个值被代入到了原来的调用表达式中,并形成了最终的延迟函数调用表达式。显然,这时的调用表达式已经与原来的表达式有所不同了。所以,Go语言会把代入参数值之后的调用表达式另行存储。以此类推,后面几次迭代所产生的延迟函数调用表达式依次为:

  1. fmt.Printf("%d ", 1)
  2. fmt.Printf("%d ", 2)
  3. fmt.Printf("%d ", 3)
  4. fmt.Printf("%d ", 4)

第二个细节是,对延迟函数调用表达式的求值顺序是与它们所在的defer语句被执行的顺序完全相反的。每当Go语言把已代入参数值的延迟函数调用表达式另行存储之后,还会把它追加到一个专门为当前外围函数存储延迟函数调用表达式的列表当中。而这个列表总是LIFO(Last In First Out,即后进先出)的。因此,这些延迟函数调用表达式的求值顺序会是:

  1. fmt.Printf("%d ", 4)
  2. fmt.Printf("%d ", 3)
  3. fmt.Printf("%d ", 2)
  4. fmt.Printf("%d ", 1)
  5. fmt.Printf("%d ", 0)

依次对它们进行求值的结果即是我们在先前展示的结果。我们再来看一个例子:

  1. func appendNumbers(ints []int) (result []int) {
  2. result = append(ints, 1)
  3. defer func() {
  4. result = append(result, 2)
  5. }()
  6. result = append(result, 3)
  7. defer func() {
  8. result = append(result, 4)
  9. }()
  10. result = append(result, 5)
  11. defer func() {
  12. result = append(result, 6)
  13. }()
  14. return result
  15. }

如果我们对appendNumbers函数进行调用并以[]int{0}作为参数值,那么它的结果值总会是[]int{0, 1, 3, 5, 6, 4, 2}。这再次说明了多个延迟函数之间的执行顺序。读者可以试着按照我们刚刚讲到的两个细节对这个函数的执行过程进行分析,并验证上述结果值。

现在我们再来虑一个问题,如果我们把printNumbers函数的声明修改为:

  1. func printNumbers() {
  2. for i := 0; i < 5; i++ {
  3. defer func() {
  4. fmt.Printf("%d ", i)
  5. }()
  6. }
  7. }

那么,执行它又会使标准输出上出现什么样的内容呢?答案是:5 5 5 5 5。为什么会是这样呢?

我们说过,在defer语句被执行的时候传递给延迟函数的参数都会被求值,但是延迟函数调用表达式并不会在那时被求值。当我们把

  1. defer fmt.Printf("%d ", i)

改为

  1. defer func() {
  2. fmt.Printf("%d ", i)
  3. }()

之后,虽然变量i依然是有效的,但是它所代表的值却已经完全不同了。让我们来简要地分析一下。在for语句的迭代过程中,其中defer语句被执行了5次。但是,由于我们并没有给延迟函数传递任何参数,所以Go语言运行时系统也就不需要对任何作为延迟函数的参数值的表达式进行求值(因为它们根本不存在)。在for语句被执行完毕的时候,共有5个延迟函数调用表达式被存储到了它们的专属列表中。注意,被存储在专属列表中的是5个相同的调用表达式:

  1. func() {
  2. fmt.Printf("%d ", i)
  3. }()

printNumbers函数的执行即将结束的时候,那个专属列表中的延迟函数调用表达式就会被逆序地取出并被逐个地求值。然而,这时的变量i已经被修改为了5(请查看printNumbers函数中的那条for子句)。因此,对5个相同的调用表达式的求值都会使标准输出上打印出5。这也就得出了我们先前展示出的那个答案。

那么我们怎么才能修正这个问题呢?很简单,我们可以把printNumbers函数中的defer语句修改为

  1. defer func(i int) {
  2. fmt.Printf("%d ", i)
  3. }(i)

可以看到,我们虽然还是以匿名函数作为延迟函数,但是却为这个匿名函数添加了一个参数声明,并在代表调用操作的圆括号中加入了作为参数的变量i。这样,在defer语句被执行的时候,传递给延迟函数的这个参数i就会被求值。最终的延迟函数调用表达式也会类似于:

  1. func(i int) {
  2. fmt.Printf("%d ", i)
  3. }(0)

又因为延迟函数声明中的参数i屏蔽了在for语句中声明的变量i,所以在延迟函数被执行的时候,其中那条打印语句中所使用的i的值即为传递给延迟函数的那个参数值。

综上所述,最后这个版本的printNumbers函数的执行效果与第一个版本的printNumbers函数的执行效果是相同的。请读者对最后一个版本的printNumbers函数进行分析,并以求值顺序列出相应的延迟函数调用表达式。

最后,我们再来说说与延迟函数有关的另外一些小技巧。首先,如果延迟函数是一个匿名函数,并且在外围函数的声明中存在命名的结果声明,那么在延迟函数中的代码是可以对命名结果的值进行访问和修改的。请看下面的代码:

  1. func modify(n int) (number int) {
  2. defer func() {
  3. number += n
  4. }()
  5. number++
  6. return
  7. }

如果我们调用modify函数并传递给它的参数值为2,那么它的结果值总会是3。因为语句number++number += n被先后地执行了。

其次,虽然在延迟函数的声明中可以包含结果声明,但是其返回的结果值会在它被执行完毕时被丢弃。因此,作为惯例,我们在编写延迟函数的声明的时候不会为其添加结果声明。另一方面,推荐以传参的方式提供延迟函数所需的外部值。作为总结,请看下面的示例:

  1. func modify(n int) (number int) {
  2. defer func(plus int) (result int) {
  3. result = n + plus
  4. number += result
  5. return
  6. }(3)
  7. number++
  8. return
  9. }

如果我们在调用modify函数的时候同样以2作为参数值,那么它的结果值总会是6。我们可以把想要传递给延迟函数的参数值依照规则放入到那个代表调用操作的圆括号中,就像调用普通函数那样。另一方面,虽然我们在延迟函数的函数体中返回了结果值,但是却不会产生任何效果。

好了,我们在本小节讲述的每一个知识点对于正确编写defer语句来说都是至关重要的。读者需要通过一定的练习才能够真正掌握defer语句的使用方法。由于本小节中的示例有限,所以请读者亲自动手去编写一些defer语句并进行相应的实例分析,这样才能真正记住和理解本小节所讲的内容。