一文让你彻底搞懂 defer 的运行原理

什么是 defer

如果熟悉 Python 的话,会发现 defer 在某种程度上有点类似于 Python 的上下文管理。Go 的 defer 是一种延迟调用的机制,可以让一些操作在当前函数执行完毕(即使出错)后执行。

因此 defer 对于 IO 流操作很有用,因为 IO 流操作结束之后是需要 close 的,而程序员很容易忘记。所以 defer 是个好东西,经常用于资源清理、文件关闭、锁释放等等。

defer 的用法

下面来看看 defer 的用法,defer 在使用上很简单,直接把一个函数调用放在它后面即可。

package main

import "fmt"

func main() {
    defer fmt.Println(123)
    fmt.Println(456)
    /*
       456
       123
    */
}

我们看到 123 是在 456 之后打印的,defer 可以看做是把后面的函数调用压入了一个栈中,等到当前函数执行完毕,再把栈里面的函数调用依次取出来执行。既然是栈,那么如果有多个 defer,执行顺序我想就不需要再解释了。

package main

import "fmt"

func main() {
    defer fmt.Println(123)
    defer fmt.Println(456)
    defer fmt.Println(789)
    /*
       789
       456
       123
    */
}

另外即使函数在运行时出错了,defer 依旧会执行,这样就保证了 IO 句柄的释放。

我们知道如果程序 panic 了,那么可以使用 recover 恢复,而 recover 必须出现在 defer 语句中。当然不写在 defer 语句中也可以,但是没有意义。因为只有等程序 panic 了,执行 recover 才有意义,所以要将 recover 写在 defer 中。

package main

import "fmt"

func main() {
    defer fmt.Println("a")
    // 不仅要出现在 defer 语句中,还必须是在匿名函数里面
    // 写成 defer recover() 是没有效果的
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    defer fmt.Println("b")
    
    panic("xxx")
    fmt.Println("c")
    /*
       b
       xxx
       a
    */
}

即便出错,也依旧会执行 defer 语句,按照栈的先进后出顺序执行,碰到 recover 进行恢复。另外如果程序 panic 了,那么即便 recover 了,panic 后面的代码也不会执行。在执行完所有 defer 之后就直接退出了,所以最下面的 "c" 是不会打印的。另外 recover 一定要出现在可能引发 panic 的代码之前,否则也是没用的。

defer 的陷阱

使用 defer 的时候容易掉坑里面,来看个例子:

package main

import "fmt"

func main() {
    i := 1
    defer fmt.Println("第一个defer:", i)
    defer func() {
        fmt.Println("第二个defer:", i)
    }()
    i = 2
    /*
       第二个defer: 2
       第一个defer: 1
    */
}

为什么会出现这种结果呢?其实是这样的,首先 defer 的后面必须跟一个函数调用,尽管它是先压入栈中,但参数却是提前确定好了的。比如第一个 defer,我们知道 Go 的函数参数是值传递,那么会把 i 的值拷贝一份传递给 Println,此时参数就已经确定了,于是想打印,这时候 defer 阻止了它:"老铁,别急,先入栈",但是此时参数就已经确定了。

可对于第二个 defer 来说,它是在匿名函数里面的,这个匿名函数没有参数。而当匿名函数执行、打印的时候,才会去找变量 i 到底是谁,显然结果是 2。


再来看个复杂点的例子:

package main

import "fmt"

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    x := 1
    y := 2
    defer calc("AA", x, calc("A", x, y))
    x = 10
    defer calc("BB", x, calc("B", x, y))
    y = 20
    
    /*
       A 1 2 3
       B 10 2 12
       BB 10 12 22
       AA 1 3 4
    */
}

为什么会是这个结果,我们来分析一下。

首先 defer 里面的参数要在入栈之前提前确定好,因此在遇到第一个 defer 的时候,是不是要执行一下calc("A", x, y)呢,因为它是外层 calc 函数的参数,所以要在入栈之前先确定好。而它在执行的时候,显然会先打印:A 1 2 3,然后返回 3。那么第一个 defer 后面要执行的函数就变成了calc("AA", 1, 3)

同理第二个 defer,会先执行calc("B", x, y),此时 x 和 y 分别为 10 和 2,从而打印B 10 2 12,返回一个12,那么第二个 defer 后面要执行的函数就变成了calc("BB", 10, 12)

然后按照 defer 的顺序,显然会打印BB 10 12 22AA 1 3 4,所以最终的结果就如代码所示。

defer 和 return

我们知道如果程序不报错,那么 defer 会在函数执行完毕之后执行,其实这句话不太准确。因为 defer 是在函数的 return 语句执行到一半的时候才开始执行的,所以 Go 的 return 不是原子性的。

我们举几个例子说明一下。

package main

import "fmt"

func foo() int {
    x := 5
    defer func() {
        x++
    }()
    return x
}

func main() {
    fmt.Println(foo()) // 5
}

Go 函数的返回值是可以起名字的,如果没有起名字,那么你可以认为 Go 编译器默认给你的返回值起了一个名字,假设就叫RET吧。当我们 return x 的时候,相当于把 x 的值交给了 RET,正准备返回时发现还有 defer,于是执行 defer,执行完毕之后将返回值返回。即便在 defer 里面将 x 的值增加了也不影响,因为返回的是 RET 的值,而这个 RET 是 5,所以返回的也是 5。

package main

import "fmt"

func foo() (x int) {
    defer func() {
        x++
    }()
    return 5
}

func main() {
    fmt.Println(foo()) // 6
}

惊了,我们 return 5,居然返回了 6。不过仔细看看返回值定义的话就能发现问题,这里给返回值起了一个名字叫 x。还记得吗?我们说如果返回值没有名字,那么你可以认为 Go 编译器给你的返回值取了个名字。

但我们这里定义了名字 x,那么return 5就等价于x = 5; return x。但是在return x之前,执行了 defer,导致 x 变了,那么 return 的结果就变了之后的 x 的值。之前说了,Go 的 return 不是原子性的,是会被 defer 打断的。

package main

import "fmt"

func foo() (y int) {
    x := 5
    defer func() {
        x++
    }()
    return x
}

func main() {
    fmt.Println(foo()) // 5
}

这里我们给返回值起名为 y,那么return x等价于y = x; return y。而y = x执行完之后,会先执行 defer,但此时改变的是 x 的值,与 y 无关,所以return y的结果还是 5。

package main

import "fmt"

func foo() (x int) {
    defer func(x int) {
        x++
    }(x)
    return 5
}

func main() {
    fmt.Println(foo()) // 5
}

和第二个例子类似,只不过里面接收了参数。但 Go 的参数传递是值传递,所以此时的 x 是一个拷贝,里面的 x 和外面的 x 无关,因此返回值还是 5。而且我们发现匿名函数里面的形参 x 就是误导人的,故意叫 x,其实叫 y、叫 z 都是一样的。

但如果参数是一个指针类型、然后里面是 *x++、而我们传递的也是 &x 的话,那么返回值还是会变成 6 的。

当然这不是最关键的,这里我想问一下,如果我在代码中的 x++ 下面打印一下 x,那么这个 x 会是多少呢?如果回答 6 的话,那么要么是你不认真,要么是你还没真正了解 defer。仔细分析一下,首先当 return 5 的时候,才会给 x 赋值为 5,但初始的话 x 显然是一个零值,int 类型的零值是 0。

我们说虽然 defer 是先入栈、后执行,但参数会提前确定好,那么匿名函数接收的实参 x 是多少呢?显然是 0。而这个 x 的值显然是入栈之前就已经确定好的,那么当 x++ 执行之后,再打印 x 的话就是 1 了。

posted @ 2019-08-26 18:12  古明地盆  阅读(2366)  评论(2编辑  收藏  举报