Go面试:从一道判断题来谈panic和defer的调用机制和执行顺序

4 篇文章 0 订阅

网上有一道关于panic和defer的判断题:“【中级】当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点,该携程结束,然后终止其他所有携程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数()”

这道题的考察点很多,要理解整个panic和defer的实现机制才能把这道题完全答对。首先把这段话切分成几个点:

  1. 显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数(引用空指针、下标越界的情况这里暂时不写)
  2. 调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点
  3. 该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数
     

1)先触发panic函数还是先调用延迟函数

如下例所示,example.go

package main

import (
        "fmt"
)

func defunc1() {
        fmt.Printf("defunc1 is called\n")
}

func test() {
        defer defunc1()
        panic("Not working!!")
}

func main() {
        test()

}

输出:

[root@dev example]# go run example.go
defunc1 is called
panic: Not working!!

goroutine 1 [running]:
main.test()
        /home/go-test/src/example/example.go:13 +0x5f
main.main()
        /home/go-test/src/example/example.go:17 +0x20
exit status 2

从输出来看defunc1先打印输出,而panic后打印输出,貌似defunc延迟函数先调用。可是,如果不先调用panic,那正常的执行流程又是怎么被中断的呢,这是一个矛盾。
下面我们来看一下这段代码的汇编代码,为了简化汇编代码,稍作改动把对fmt的引用及调用换成对全局变量的赋值:

package main

var g int

func defunc1() {
        g = 13
}

func test() {
        defer defunc1()
        panic("Not working!!")
}

func main() {
        g = 17
        test()

}

通过命令“GOOS=linux GOARCH=386 go tool compile -S example.go > example.S”得到汇编代码,其中test函数的代码片段如下:

"".test STEXT size=96 args=0x0 locals=0x28
 26         0x0000 00000 (example.go:9)     TEXT    "".test(SB), ABIInternal, $40-0
 27         0x0000 00000 (example.go:9)     MOVL    TLS, CX
 28         0x0007 00007 (example.go:9)     MOVL    (CX)(TLS*2), CX
 29         0x000d 00013 (example.go:9)     CMPL    SP, 8(CX)
 30         0x0010 00016 (example.go:9)     JLS     89
 31         0x0012 00018 (example.go:9)     SUBL    $40, SP
 32         0x0015 00021 (example.go:9)     FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 33         0x0015 00021 (example.go:9)     FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 34         0x0015 00021 (example.go:9)     FUNCDATA        $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
 35         0x0015 00021 (example.go:10)    PCDATA  $0, $0
 36         0x0015 00021 (example.go:10)    PCDATA  $1, $0
 37         0x0015 00021 (example.go:10)    MOVL    $0, ""..autotmp_0+8(SP)
 38         0x001d 00029 (example.go:10)    PCDATA  $0, $1
 39         0x001d 00029 (example.go:10)    LEAL    "".defunc1·f(SB), AX
 40         0x0023 00035 (example.go:10)    PCDATA  $0, $0
 41         0x0023 00035 (example.go:10)    MOVL    AX, ""..autotmp_0+24(SP)
 42         0x0027 00039 (example.go:10)    PCDATA  $0, $1
 43         0x0027 00039 (example.go:10)    LEAL    ""..autotmp_0+8(SP), AX
 44         0x002b 00043 (example.go:10)    PCDATA  $0, $0
 45         0x002b 00043 (example.go:10)    MOVL    AX, (SP)
 46         0x002e 00046 (example.go:10)    CALL    runtime.deferprocStack(SB)  //延迟函数入先进后出队列
 47         0x0033 00051 (example.go:10)    TESTL   AX, AX
 48         0x0035 00053 (example.go:10)    JNE     79
 49         0x0037 00055 (example.go:11)    PCDATA  $0, $1
 50         0x0037 00055 (example.go:11)    LEAL    type.string(SB), AX
 51         0x003d 00061 (example.go:11)    PCDATA  $0, $0
 52         0x003d 00061 (example.go:11)    MOVL    AX, (SP)
 53         0x0040 00064 (example.go:11)    PCDATA  $0, $1
 54         0x0040 00064 (example.go:11)    LEAL    ""..stmp_0(SB), AX
 55         0x0046 00070 (example.go:11)    PCDATA  $0, $0
 56         0x0046 00070 (example.go:11)    MOVL    AX, 4(SP)
 57         0x004a 00074 (example.go:11)    CALL    runtime.gopanic(SB) // panic编译成对gopanic的调用
 58         0x004f 00079 (example.go:10)    XCHGL   AX, AX
 59         0x0050 00080 (example.go:10)    CALL    runtime.deferreturn(SB) //只要有defer函数,编译器就会在函数返回前插入对延迟函数队列的执行,直到队列为空
 60         0x0055 00085 (example.go:10)    ADDL    $40, SP
 61         0x0058 00088 (example.go:10)    RET  //函数返回
 62         0x0059 00089 (example.go:10)    NOP
 63         0x0059 00089 (example.go:9)     PCDATA  $1, $-1
 64         0x0059 00089 (example.go:9)     PCDATA  $0, $-1
 65         0x0059 00089 (example.go:9)     CALL    runtime.morestack_noctxt(SB)
 66         0x005e 00094 (example.go:9)     JMP     0

这段汇编里有3个重要函数,都在runtime.go源文件里。

  1. runtime.deferprocStack: 对应的是(example.go:10)defer defunc1这一句,把延迟函数加入到该协程对应的一个延迟函数链表,每个协程各自维护一个延迟函数链表。
     func deferprocStack(d *_defer) {
             gp := getg() //获取当前协程,不同的协程维护不同的延迟函数链表
    
             ...
             
             //把d置成新的链表表头
             *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) 
             *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
    
             return0()
     }

     

  2. runtime.gopanic,对应的是(example.go:11)panic("Not working!!")这一句,如果是系统级别异常(如panic during malloc)则直接退出进程不会执行任何延迟函数。如果是非系统级别异常,则按先进后出顺序调用runtime.deferreturn来逐个执行本协程的延迟函数链表,如果某个被执行的延迟函数包含有对recover的调用,则恢复正常。如果都没有对recover的调用,则打印输出panic的消息如这里的"Not working!!",然后打印stack trace,最后调用exit(2)结束进程。
    func gopanic(e interface{}) {
    	gp := getg() //获取当前协程
    	...
    
    	for {
    		d := gp._defer //获取当前协程的延迟函数链表头
    	        
                    ...
                    //调用延迟函数
    		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
                    ...
    
    	        gp._defer = d.link //移动到延迟函数链表的下一个节点	
    		freedefer(d) //释放链表节点d
    		if p.recovered {
    			...
    			mcall(recovery) //恢复协程
    			...
    		}
    	}
    
    	preprintpanics(gp._panic)  //打印panic消息
    
    	fatalpanic(gp._panic) // 打印stack trace,退出进程
    	*(*int)(nil) = 0      // not reached
    }
    
    func fatalpanic(msgs *_panic) {
            ...
    	systemstack(func() {
    		exit(2)  //退出进程
    	})
    
    	*(*int)(nil) = 0 // not reached
    }

     

  3. runtime.deferreturn,这个是编译器追加的,如果已经有panic发生,则在第2步就已经调用过deferreturn了,延迟函数链表已经为空。如果没有panic发生,则执行延迟函数链表里的延迟函数。
     

由此可见,在调用顺序上是先调用panic对应的gopanic函数, 由gopanic函数去执行延迟函数,所以“先触发panic函数的执行,然后调用延迟函数”是正确的。

2)子函数是否会把panic传递给父函数

同一个协程的所有延迟函数会维护在同一个链表当中,当子函数触发panic即调用runtime.gopanic函数时,它会执行完链表里的所有延迟函数,直到遇到某个延迟函数包含有对recover的调用,否则直接退出进程,注意是退出进程而不是协程。因此,panic是不会在父子函数之间传递的。

所以“调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点”这一句是错误的。

3)触发panic的协程是如何停止所有协程的

gopanic函数调用fatalpanic,再由fatalpanic调用systemstack,systemstack最终调用exit(2),退出“进程”,而不是仅仅退出“协程”,其它协程的延迟函数不会被执行。这里的exit在不同的操作系统上对应的函数不同,例如在windows上它对应的是

src/runtime/os_windows.go +556调用windows操作系统函数_ExitProcess(Ends the calling process and all its threads.)退出进程。

func exit(code int32) {
        atomic.Store(&exiting, 1)
        stdcall1(_ExitProcess, uintptr(code))
}

下面的代码可以验证其它协程的延迟函数是否会执行:

package main

import (
        "fmt"
        "time"
)

func defunc1() {
        fmt.Printf("defunc1 is called\n")
}

func defunc2() {
        if r := recover(); r != nil {
                fmt.Println("Recovered from panic that is defunc2: ", r)
        }

        fmt.Printf("defunc2 is called\n")
}

func test() {
        defer defunc1()
        panic("Not working!!")
}

func main() {
        defer defunc2()

        go func() {
                test()
        }()

        time.Sleep(3 * time.Second)
        fmt.Printf("main exit -----------")
}

输出:

defunc1 is called
panic: Not working!!

goroutine 6 [running]:
main.test()
        /home/zhoupeng/go-test/src/example/m.go:22 +0x5f
main.main.func1()
        /home/zhoupeng/go-test/src/example/m.go:29 +0x20
created by main.main
        /home/zhoupeng/go-test/src/example/m.go:28 +0x6d
exit status 2

可见,如果另起一个协程来调用test(),则当test()触发panic时,main函数里的延迟函数不会被执行。如果上面的代码对test()的调用改成放在main函数所在的协程:

func main() {
        defer defunc2()

        //go func() {
                test()
        //}()

        time.Sleep(3 * time.Second)
        fmt.Printf("main exit -----------")
}

则输出:

defunc1 is called
Recovered from panic that is defunc2:  Not working!!
defunc2 is called

所以“该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数”这句是错误的。

4)panic之后的defer函数是否会被执行

题目里并没有问这一点,但这也是panic/defer机制的一部分。例如在panic之后再增加两个defer:defer defunc3()和defer defunc4()

func test() {
        defer defunc1()
        panic("Not working!!")
        defer defunc3()
}

func main() {
        defer defunc2()

        test()

        defer defunc4()

}

从汇编代码,Go的源码和输出结果来看,defer defunc3()由于和panic处在同一个test()函数并且在panic之后,因此会被编译器在编译过程中直接忽略掉;而defer defunc4()会被编译成runtime.deferprocStack,但是在panic触发后进程直接退出,defunc4的runtime.deferprocStack还没有被调用,还没有进入延迟队列,所以不会被执行。

5) panic触发之后defer函数里再次触发新的panic,之前的panic怎么处理

例如在下面的代码中,panic1触发之后执行最后一个defer再触发panic2, panic2触发后再执行倒数第二个defer进而触发panic3,而Go的机制是如果当前defer函数是由之前的panic触发的并且当前defer函数会触发新的panic,则前一个panic停止执行。也就是说下面代码中当panic2触发时,panic1的执行过程将停止,当panic3触发时panic2的执行过程将停止,所以最终能被recover捕获到的只有panic3。

func main() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Println(err)
                }
        }()
        defer func() {
                panic("panic3")
        }()
        defer func() {
                panic("panic2")
        }()
        panic("panic1")
}

输出: 

panic3

这部分源码在“src/runtime/panic.go”的gopanic函数里。它的注释写的比较清楚,当我们因为触发了一个新的panic而再次运行到这里时,如果这个defer是由之前的panic调用起来的,则前一个panic停止执行。

                // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
                // take defer off list. The earlier panic or Goexit will not continue running.
                if d.started {
                        if d._panic != nil {
                                d._panic.aborted = true
                        }
                        d._panic = nil
                        d.fn = nil
                        gp._defer = d.link
                        freedefer(d)
                        continue
                }

注:以上测试的Go版本是1.13.8

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值