什么是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?22
和AA?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啦。
原文地址:https://www.cnblogs.com/traditional/p/12206996.html