轻松掌握golang的defer机制

Posted traditional

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了轻松掌握golang的defer机制相关的知识,希望对你有一定的参考价值。

什么是defer?

如果熟悉python的话,会感觉defer在某种程度上有点类似于python中的上下文管理,golang中的defer是golang提供的一种延迟调用的机制,可以让一个函数在当前函数执行完毕(即使出错)后执行。

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

defer的用法

直接把一个函数调用放在defer后面即可。

f, err := os.Open(filepath)
if err != nil {
    panic(err)
}
defer f.Close()
//我们知道f.Close()是会返回一个error的,而defer后面只能是一个函数调用,不能是赋值语句
//不可以是defer _ = f.Close(),那怎么办呢?使用匿名函数即可
//defer func(){_ = f.Close()}()

再比如:

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的陷阱

来看个例子

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,我们知道golang的函数参数是值传递,那么会把i的值拷贝一份传递给Println,此时参数就已经确定了,于是想打印,这时候defer阻止了它:"老铁,别急,先入栈",但是此时参数就已经确定了。可对于第二个defer来说,我们看到它是在匿名函数里面的,这个匿名函数没有参数,那么不好意思,当匿名函数执行、从而打印的时候,这是的i就是最终的i了,所以是2。第一个defer记得变量i初始的模样,已经印在了脑海里,所以最后即使i变了,第一个defer还是记得它清纯的模样。但是第二个defer就不一样了,对于它来说,它是第一次见到i,所以打印的就是i最终的值。

再来看个复杂点的例子

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) ->?calc("B", 10, 2),从而打印B?10?2?12,返回一个12,那么第二个defer后面要执行的函数就变成了calc("BB", 10, 12)

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

defer和return

我们知道如果程序不报错,那么defer会在函数执行完毕之后执行,但是实际上我们说对了一半。什么时候函数才算执行完毕了,我们可以认为当return语句执行完之后这个函数就算执行完毕了。但是defer就是在return语句执行到一半的时候才开始执行的,所以我们算是说对了一半吧。因此golang的return不是原子性的

技术图片

package main

import "fmt"

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

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

我们知道golang的返回值是可以起名字的,如果没有起名字,那么你可以认为golang给你的返回值起了一个名字,假设就叫mmp吧,当我们return x的时候,相当于把x的值交给了mmp,正准备返回的时候返现还有defer,于是执行defer,执行完毕之后将返回值返回。即便这个x的值增加了,但是返回的是mmp的值,而这个mmp是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。还记得吗?我们说如果返回值没有名字,那么你可以简单地认为golang给你的返回值取了个名字,当然我们这里定义了名字x,那么return?5等价于,x?= 5;?return?x,但是在return?x之前,x变了,那么return的也是变了之后的x的值,之前说了,golang的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
}

和第二个例子类似,只不过里面接收了参数,但golang是值传递,所以此时的x是一个拷贝,因此值还是原来的5。而且我们发现匿名函数里面的参数x就是误导人的,故意叫x,其实叫y、叫z都是一样的。但是,如果参数是一个指针类型、然后里面是*x++、而我们传递的也是&x的话,那么返回值还是会变成6的。当然这都不重要,我想问一下,如果我在上面的x++下面,打印一下x的话,那么这个x会是多少呢?如果回答6的话,那么要么是你不认真,要么是你还没真正了解defer。仔细分析一下,首先当return?5的时候,才会给x赋值为5,但是初始的话x显然是一个零值,int的话就是0。而x又是defer后面的匿名函数的一个参数,我们说defer虽然入栈、最后执行,但是参数是多少是会先确定的,那么参数x是多少呢?显然就是0啊,那么当x++执行的时候,这个x的值显然是入栈之前就已经确定好的、给参数x传递的值,也就是0,那么++之后,最后就打印1啦。

以上是关于轻松掌握golang的defer机制的主要内容,如果未能解决你的问题,请参考以下文章

Golang的错误处理机制 defer recover()

探究 Go 语言 defer 语句的三种机制

golang之匿名函数结合defer

GoLangpanic defer recover 深入理解

golang中defer的使用规则

golang学习之defer