Go 匿名函数 defer 函数


原文链接: Go 匿名函数 defer 函数

defer关键字 · 深入解析Go

  1. 多个 defer 的执行顺序为"后进先出"
  2. 所有函数在执行 RET 返回指令之前,都会先检查是否存在 defer 语句,若存在则先逆序调用 defer 语句进行收尾工作再退出返回;

  3. 匿名返回值是在 return 执行时被声明,有名返回值则是在函数声明的同时被声明,因此在 defer 语句中只能访问有名返回值,而不能直接访问匿名返回值;

    1. return 其实应该包含前后两个步骤:
      第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);
      第二步是调用 RET 返回指令并传入返回值,而 return 则会检查 defer 是否存在,若存在就先逆序插播 defer 语句,最后 RET 携带返回值退出函数;

‍‍因此,‍‍defer、return、返回值三者的执行顺序应该是:

  1. return 最先给返回值赋值;
  2. 接着 defer 开始执行一些收尾工作; ** 有命名返回值的情况, 修改返回值 **
  3. 最后 RET 指令携带返回值退出函数。

defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,
return xxx 会被改写成:

返回值 = xxx
调用defer函数
空的return

先看例1,它可以改写成这样:

func f() (result int) {
     result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() {    //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}

result = 0
result++
return
所以这个返回值是1。

再看例2,它可以改写成这样:

func f() (r int) {
     t := 5
     r = t //赋值指令
     func() {        //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令
}

r = t =5
return
所以这个的结果是5。

最后看例3,它改写后变成:

func f() (r int) {
     r = 1  //给返回值赋值
     func(r int) {        //这里改的r是传值传进去的r,不会改变要返回的那个r值
          r = r + 5
     }(r)
     return        //空的return
}

r=1
return
所以这个例子的结果是1。

defer 执行规则

规则一 当 defer 被声明时,其参数就会被实时解析

我们通过以下代码来解释这条规则:

func a() int {
    i := 0     
    // 因为 i=0,所以此时就明确告诉 golang 在程序退出时,执行输出 0 的操作
    defer fmt.Println(i)  // i 作为副本传入, 立即解析
    i++
    return  // 0
}

上面我们说过,defer 函数会在 return 之后被调用。那么这段函数执行完之后,是不用应该输出 1 呢?

读者自行编译看一下,结果输出的是 0. why?

这是因为虽然我们在 defer 后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量 (i) 在 defer 被声明的时候,就已经确定其确定的值了。
为了更为明确的说明这个问题,我们继续定义一个 defer:

func a() {
    i := 0
    defer fmt.Println(i) // 输出 0,因为 i 此时就是 0
    i++
    defer fmt.Println(i) // 输出 1,因为 i 此时就是 1
    return
}

通过运行结果,可以看到 defer 输出的值,就是定义时的值。而不是 defer 真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出 1,在输出 0 呢? 看下面的规则二。

规则二 defer 执行顺序为先进后出

当同时定义了多个 defer 代码块时,golang 安装先定义后执行的顺序依次调用 defer。不要为什么,golang 就是这么定义的。我们用下面的代码加深记忆和理解:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i) //3210
    }
}

在循环中,依次定义了四个 defer 代码块。结合规则一,我们可以明确得知每个 defer 代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了 3210.

规则三 defer 可以读取有名返回值

func c() (i int) {
    defer func() { i++ }()
    return 1
    // 输出结果是 2
}

在开头的时候,我们说过 defer 是在 return 调用之后才执行的。 这里需要明确的是 defer 代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer 的作用域仍然在 c 函数之内。因此 defer 仍然可以读取 c 函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢....)。

当执行 return 1 之后,i 的值就是 1. 此时此刻,
defer 代码块开始执行,对 i 进行自增操作。 因此输出 2.

掌握了 defer 以上三条使用规则,那么当我们遇到 defer 代码块时,就可以明确得知 defer 的预期结果。

Golang Pointers on the Heap

二、Go defer 表达式

Defer, Panic, and Recover

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

defer 语句会存放在一个列表中,在其它函数语句执行 return 之后再执行。

三、匿名函数

Go 不能在函数内部显式嵌套定义函数,但是可以定义一个匿名函数,Go 通过匿名函数实现闭包(闭包,通过 “捕获” 自由变量的绑定对函数文本执行的 "闭合" 操作)。

package main

import "fmt"

func f(i int) func() int {
    return func() int {
        i++
        return i
    }
}

func main() {
    m1 := f(2)
    fmt.Println(m1())    // 指针指向 i, i = 2, 输出 3
    fmt.Println(m1())    // 指针指向 i,i = 3,输出 4

    m2 := f(2)
    fmt.Println(m2())    // 指针指向 另外一个 i,i = 2,输出 3
}

四、实例

前段时间在 twitter 看到一个例子,问以下代码应该输出什么,后来分析之后才知道结果,代码如下:

package main

import "fmt"

type number int

func (n number) print() {
    fmt.Printf("输出 number 值 print: %v\n", n)
}
func (n *number) pprint() {
    fmt.Printf("输出 number 值 pprint: %v\n", *n)
}

func main() {
    var n number

    defer n.print()
    defer n.pprint()
    defer func() {
        n.print()
    }()
    defer func() {
        n.pprint()
    }()

    n = 3
}

输出结果如下:

➜ ~ go run test.go
输出 number 值 pprint: 3
输出 number 值 print: 3
输出 number 值 pprint: 3
输出 number 值 print: 0

根据 defer 的后进先出原则,4 个 defer 语句的执行顺序为倒序的,最后两个属于闭包,很好的理解输出为 3。defer n.pprint() 语句因为是指针,所以输出结果依然为 3。defer n.print() 为何为 0,这个相对较难理解,根据上文的说明,var n number 执行后,n 被初始化为 0,之后 defer 语句就被传入内存的 list 中,因此 defer n.print() 输出值为 0。

`