每次学习并整理一个Golang的知识点,每天进步一点点🤔。今天我们来看下defer函数相关的。

代码片段

看如下这段代码,并猜测输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func deferCall() {
    defer func() { fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()

    panic("触发异常")
}

func main() {
    fmt.Println("答案:")
    deferCall()
}

思考后看答案

思考后看答案


答疑解惑

代码执行结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
答案:
打印后
打印中
打印前
panic: 触发异常

goroutine 1 [running]:
main.deferCall()
...

我们可以看到一个函数中可以同时存在多个defer语句,需要注释意的是,defer语句的调用同栈一样,遵循先进后出的原则,最后一个defer语句将最先被执行。不过我们这里的代码片段只做简单的展示用,当你需要为defer语句到底哪个先执行这种细节而烦恼的时候,说明你的代码架构可能需要调整一下了。

上述结果中,我们也看到panic是最后执行的。当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立即终止,但函数中,之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直到所࡚属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用panic()函数时传入的参数,这个过程称为错误处理流程。

深入源码

当然最好的答案,莫过于看下golang中panic的实现源码,这有助于我们更好的记忆;

panic源码部分展示:

 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
func Goexit() {
    gp := getg() // getg()返回当前协程的 g 结构体指针,g 结构体描述 goroutine
    var p _panic  // 为Goexit创建一个panic对象
    p.goexit = true
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
    for {
        d := gp._defer // 获取当前协程defer链表的头节点
        if d == nil {
            break  // 当前协程的defer都被执行后,defer链表为,此时退出for循环
        }
        if d.started {  // 发生panic后,在defer中又遇到panic(),则会进入这个代码块
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            if !d.openDefer {
                d.fn = nil
                gp._defer = d.link
                freedefer(d)
                continue
            }
        }
        d.started = true
        // 此处省略
    }
}

从上述不完整的代码片段中,我们也可以看到,发生panic时,先进入了一个for循环,当defer都被执行完成后,再退出循环,继续后面的逻辑匹配。相信看到这里,panic和defer的执行顺序就一目了然了。


欢迎转发和评论。更多优质原创文章,欢迎关注微信公众号“IYue爱月”或扫描下方二维码:

xujpxm_qrcode_gh