延迟函数(defer)
延迟函数,顾名思义,是在之后执行的函数,那么具体怎么个之后法呢?在函数调用的过程中,可以理解为调用函数的过程是一个链表,a->b->c,整个调用过程是先a再b再c,而延迟函数就是在调用a的程序过程中写入一个延迟执行,在a函数return之后添加一个函数调用。因此,defer的常用功能为释放资源、关闭连接、捕获错误等,这是一种语言保护机制。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func Open() {
f, err := os.Open("")
if err != nil {
return
} // 如果此处Open函数没有问题,继续向下
s, err := os.Create("")
if err != nil {
return // 如果此处发生error,那么直接return,后面的两个close都不执行
}
f.Close()
s.Close()
return
}
|
上面这段代码中如果Open函数正常执行,但是Create函数错了,返回错误时下面的close函数没有办法执行,就会导致f没有关闭,但是我们又不能直接写在上面,因为还没对f进行操作就关闭了文件,所以这里就要使用延迟函数了。
1
2
3
4
5
6
7
|
func Open() {
f, err := os.Open("")
if err != nil {
return
}
defer f.Close() // 注意这里还有一个坑,下面再介绍
}
|
接下来我们就介绍defer的使用:
defer的执行参数
defer的执行参数在defer写入的位置就已经确定好了,也就是说,defer并不只是单单在return之后执行,而是在准备延迟调用的时候,就已经把参数一并压入堆栈中等待执行。
1
2
3
4
5
6
7
|
func main() {
x := 5
defer fmt.Println(x) // 这个时候已经将x=5传参给print函数了
x++
fmt.Println(x)
// 结果打印为6 5 (注意正常是换行打印,为了看起来方便就这么写了,后面也一样)
}
|
思考一下这样写结果呢?
1
2
3
4
5
6
7
8
9
|
func main() {
x := 5
defer func() {
fmt.Println(x) // 这里相当于一个闭包,defer执行的是一个func(),但是func()不需要任何参数,而是去执行打印x,这时候x就打印出为6了,请区分和下面的匿名函数的问题
}()
x++
fmt.Println(x)
// 结果打印为6 6
}
|
defer的执行顺序
在知道了defer的执行参数后就需要知道defer的执行顺序,如果有多条defer语句的话,根据压入顺序,最后是逆序执行的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func main() {
x := 5
defer func() { // 最后执行
fmt.Println(x)
}()
defer func() { // 再执行
fmt.Println("ZonzeeLi")
x++
}()
defer func() { // 先执行
fmt.Println("你好")
}()
// 结果打印为 你好 ZonzeeLi 6
}
|
defer对匿名返回值的返回处理
我们在自己写一个func()的时候经常会写返回值,但是返回值时分命名和匿名两种,这在defer中又有区别了,我们看下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
func f1() int {
x := 5
defer func() {
x++
// 在这个地方加一个打印x,看看结果是多少?
}()
return x
// 这个函数返回5
}
func f2() (x int) { // 如果把这个x换成y的话,返回结果是多少?
//x = 0
defer func() {
x++
}()
return 5
// 这个函数返回6
}
func f4() (x int) { // 这个返回是多少?
defer func(x int) {
x++
// 这里加一个打印x,打印的是多少?
}(x)
return 50
// 这个结果返回为50,请自行思考,解释参考上面的defer执行参数
}
|
f1中使用的是匿名返回值,f2中使用的是命名返回值,f1的匿名返回值,相当于返回x的一个副本给f1的return,所以对x进行x++操作没有对return进行影响,而有命名返回值则不同,defer的作用域始终是在这个函数内的,所以使用的返回值就是x。
defer panic recover的配合使用
1
2
3
4
5
6
7
8
|
func test() {
panic(1)
}
func main() {
test() // 结果为panic: 1, 不会打印2
fmt.Println(2)
}
|
我们知道如果函数panic了,就直接终止运行了,后面的语句都不会执行,但是如果我们想要执行的话就需要recover,这时候defer的延迟使用就非常有帮助了。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func test() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获成功啦!")
}
}()
panic(1)
}
func main() {
test()
fmt.Println(2) // 结果为 捕获成功啦!2
}
|
另外这里要注意的是,如果连续调用panic,仅最后一个会被recover捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Println(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("you are dead")
}()
panic("i am dead")
// 结果为
// 2022/03/08 15:58:08 you are dead
// 2022/03/08 15:58:08 fatal
}
|
既然说到这,我就补充一下,defer中直接调用recover是无效的,举例如下:
1
2
3
4
5
6
|
func main() {
defer log.Println(recover()) // 无效
defer recover() // 无效
panic("i am dead")
}
|
常见的defer面试题和坑
以上将defer的几个用法介绍完了,下面我总结了在一些博主的文章中看到的问题和平常遇见的坑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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)) // 先执行了calc("A", x, y),然后再压入defer执行语句和defer需要的参数
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("AA", x, calc("A", x, y))
,他的三个参数就已经确定了,那么第三个参数是由calc("A", x, y)
的返回值决定的,所以这个时候就会先执行这一条,然后将参数一同压入栈,将延迟函数压入函数调用,下面的defer calc("BB", x, calc("B", x, y))
也同理。
问题二,for循环导致性能问题,出自《Go语言学习笔记》
1
2
3
4
5
6
7
|
func test() {
...
for i := 0;i < n;i ++ {
f, _ := os.Open("")
defer f.Close()
}
}
|
上面的代码其实没什么运行问题,只是defer的执行需要尽心很多压栈出栈以及内存使用的操作,会消耗很多资源,所以应该直接执行或者封装。
问题三,defer语句的位置,出自牛客网Go语言面试题
1
2
3
4
5
6
7
8
9
10
|
func Open() {
f, err := os.Open("")
defer f.Close()
if err != nil {
...
return
}
// defer f.Close()
...
}
|
上面的代码写法有什么问题?也正常执行延迟Close了?问题在于defer语句的位置,如果写在判断err然后return之前,如果Open发生异常,那么f就是空指针,在return之后执行defer f.Close()
就会报空指针错误,所以要在处理玩错误,return之后执行defer。
问题四,如下代码,当函数deferDemo()返回失败时,并不能destroy已create成功的资源,这一说法是否正确,出自牛客网Go语言面试题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED // 如果1失败,直接返回没有任何创建成功的资源
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED // 如果2创建失败,那么执行上面的defer,destroy已经创建的1
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED // 同理
}
defer func() {
if err != nil {
destroyResource3()
}
}()
return nil
}
|
正确答案是错误,也就是如果返回失败,可以destroy已经create的资源,解释如下,如果返回成功,不需要讨论,如果返回失败,我们假设在createResource1()
处失败,这时候2、3都没有进行创建,1也没有创建成功,直接返回,所以没有创建成功的资源,不需要destroy,如果在createResource2()
,这时候err
有了值,在返回2失败后,执行了上面1成功后压入的defer,破坏了1,下面的3也同理。
问题五,下面这段话是否正确,出自牛客网Go语言面试题
当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数。这一说法是否正确。
答案是错误的,上面的这段话需要注意的是一个地方,则先触发panic函数的执行,然后调用延迟函数
,panic是需要等defer结束后才会向上传递,出现panic,会先按照defer的逆序执行顺序,执行完之后才会执行panic。有一段大佬的完整解释为:“当内置的panic()函数调用时,外围函数或方法的执行会立即终止。然后,任何延迟执行(defer)的函数或方法都会被调用,就像其外围函数正常返回一样。最后,调用返回到该外围函数的调用者,就像该外围调用函数或方法调用了panic()一样,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。当到达main()函数时不再有可以返回的调用者,因此这个过程会终止,并将包含传入原始panic()函数中的值的调用栈信息输出到os.Stderr”,可以举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func test() {
defer fmt.Println("test.1")
defer fmt.Println("test.2")
panic("i am dead")
}
func main() {
defer func() {
log.Println(recover())
}()
test()
// 结果为
// test.2
// test.1
// 2022/03/07 15:53:27 i am dead
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func bar() (r int) {
defer func() {
r += 4 // 3. r += 4
if recover() != nil { // 4. recover()捕获成功
r += 8 // 5.r += 8 输出 13
}
}()
var f func()
defer f() // 2.先执行这个,但是因为defer语句的位置,f()报错
f = func() {
r += 2
}
return 1 // 1.先给r进行赋值
}
func main() {
println(bar()) //结果为13
}
|
答案是13,你思考对了么?这道题的关键在于那个defer f()
,此处因为defer语句的写法位置,因为var f func()
,f()是空指针,而直接进行defer f()
,在对f()赋值,所以在defer中会panic,这个就和上面说的问题三一样了,所以正确的执行顺序是,返回1给r,然后defer f()
直接panic,再向上进行,r += 4
然后再recover()
捕获了panic,执行了r += 8
。
问题七,思考结果,出自coding进阶
1
2
3
4
5
6
7
8
9
10
11
12
|
func main() {
defer func() {
fmt.Print(recover()) // 4.同理捕获到了1
}()
defer func() {
defer fmt.Print(recover()) // 2.先解析了参数,所以这里先执行了recover(),这时候还没有按照defer的逆序执行,所以先把2捕获到了
defer panic(1)
recover() // 3.有效,但是被截胡了
}()
defer recover() // 1.无效
panic(2)
}
|
结果是21,你回答对了么?我猜测多数人会回答nil1,还有人会回答nil,我这里分别解释一下,回答nil的是因为没理解recover的使用,recover必须配合defer的函数体直接调用使用,否则都会捕获无效,这个在上面都有介绍到,所以第一个defer(从下往上)是无效的,回答为nil1的是注意到了很多,但是最关键的在fmt.Print(recover())
中,recover()
作为参数被defer中的func调用,要先执行参数解析,所以2就会被先捕获到,而不是正常的先捕获经过头插法的最近panic,同理最上面的defer就捕获到了1。
延迟函数defer的基础使用就差不多介绍这些,具体太细的,以及配合panic和recover使用的需要看底层和汇编才能理解的更透彻。
Coding every day,let’s go!