【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案

前端之家收集整理的这篇文章主要介绍了【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

【Go学习】一道简单Golang面试题中关于panic和defer的执行顺序引发的惨案

题目有点夸张,标题党一把,哈哈,不过也确实是在一个小的面试中碰到这个题目,然后当时经过我反复斟酌之后,愉快的写下了一个错误的答案,回来之后,自己验证了一下,于是就有了这篇文章,大神请绕道。
废话不多说直接上题目,说有如下程序(main.go),写出运行之后的结果:

package main

import "fmt"

func main(){
    defer_call()
    fmt.Println("333 Helloworld")
}

func defer_call()  {
    defer func(){
        fmt.Println("11111")
    }()
    defer func(){
        fmt.Println("22222")
    }()

    defer func() {
        if r := recover(); r!= nil {
            fmt.Println("Recover from r : ",r)
        }
    }()

    defer func(){
        fmt.Println("33333")
    }()

    fmt.Println("111 Helloworld")

    panic("Panic 1!")

    panic("Panic 2!")

    fmt.Println("222 Helloworld")
}

我直接贴出运行结果:

111 Helloworld
33333
Recover from r :  Panic 1!
22222
11111
333 Helloworld

如果你做对了,建议跳过。其实我也只是把自己的验证过程记录如下,以便以后查阅。

我们用上一篇文章所搭建的golang的gdb调试环境来具体分析下为什么会是这个结果。

编译源代码使用以下命令,这里的-l参数的意思和上面一样,如果有需要还可以加-N参数:

/home/james/workspace/go_src/bin/go build -gcflags "-l" main.go

对这个编译方法有疑问的可以参考上一篇文章
编译后使用gdb运行:

go里面的函数符号名称的命名规则是包名称.函数名称,例如主函数的符号名称是main.main,运行时中的newobject的符号名称是runtime.newobject.
首先给主函数下一个断点,给我们第一个panic("Panic 1!")所在行下一个断点,然后运行:

单步运行之后,我们可以找到panic函数所对应的源码:

上一篇文章中所准备的源码中找到对应的文件src/rumtime/panic.go:425,即panic函数具体实现如下:

// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
    gp := getg()    // getg()返回当前协程的 g 结构体指针,g 结构体描述 goroutine
    if gp.m.curg != gp {
        print("panic: ")
        printany(e)
        print("\n")
        throw("panic on system stack")
    }

    // m.softfloat is set during software floating point.
    // It increments m.locks to avoid preemption.
    // We moved the memory loads out,so there shouldn't be
    // any reason for it to panic anymore.
    if gp.m.softfloat != 0 {
        gp.m.locks--
        gp.m.softfloat = 0
        throw("panic during softfloat")
    }
    if gp.m.mallocing != 0 {
        print("panic: ")
        printany(e)
        print("\n")
        throw("panic during malloc")
    }
    if gp.m.preemptoff != "" {
        print("panic: ")
        printany(e)
        print("\n")
        print("preempt off reason: ")
        print(gp.m.preemptoff)
        print("\n")
        throw("panic during preemptoff")
    }
    if gp.m.locks != 0 {
        print("panic: ")
        printany(e)
        print("\n")
        throw("panic holding locks")
    }

    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    atomic.Xadd(&runningPanicDefers, 1)

    for {
        d := gp._defer    // 获取当前协程defer链表的头节点
        if d == nil {
            break    // 当前协程的defer都被执行后,defer链表为空,此时退出for循环
        }

        // 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 {    // 发生panic后,在defer中又遇到panic(),则会进入这个代码
            if d._panic != nil {
                d._panic.aborted = true
            }
            d._panic = nil
            d.fn = nil
            gp._defer = d.link
            freedefer(d)  // defer 已经被执行过,则释放这个defer,继续for循环。
            continue
        }

        // Mark defer as started,but keep on list,so that traceback
        // can find and update the defer's argument frame if stack growth
        // or a garbage collection happens before reflectcall starts executing d.fn.
        d.started = true

        // Record the panic that is running the defer.
        // If there is a new panic during the deferred call,that panic
        // will find d in the list and will mark d._panic (this panic) aborted.
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

        p.argp = unsafe.Pointer(getargp(0))
        reflectcall(nil,unsafe.Pointer(d.fn),deferArgs(d),uint32(d.siz),uint32(d.siz))   // 执行当前协程defer链表头的defer
        p.argp = nil

        // reflectcall did not panic. Remove d.
        if gp._defer != d {
            throw("bad defer entry in panic")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link  // 从defer链中移除刚刚执行过的defer

        // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
        //GC()

        pc := d.pc
        sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
        freedefer(d)   // 释放刚刚执行过的defer
        if p.recovered {    // defer()中遇到recover后进入这个代码
            atomic.Xadd(&runningPanicDefers, -1)

            gp._panic = p.link
            // Aborted panics are marked but remain on the g.panic list.
            // Remove them from the list.
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil { // must be done with signal
                gp.sig = 0
            }
            // Pass information about recovering frame to recovery.
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)   // 跳转到recover()处,继续往下执行
            throw("recovery Failed") // mcall should not return
        }
    }

    // ran out of deferred calls - old-school panic now
    // Because it is unsafe to call arbitrary user code after freezing
    // the world,we call preprintpanics to invoke all necessary Error
    // and String methods to prepare the panic strings before startpanic.
    preprintpanics(gp._panic)
    startpanic()

    // startpanic set panicking,which will block main from exiting,
    // so now OK to decrement runningPanicDefers.
    atomic.Xadd(&runningPanicDefers, -1)

    printpanics(gp._panic)   // 输出panic信息
    dopanic(0)       // should not return
    *(*int)(nil) = 0 // not reached
}

上面代码虽然有些没有看懂,但是其执行流程还是比较清楚,从代码上来看,协程遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中,遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。从执行顺序上来看,实际上是按照先进后出的顺序执行defer。这个时候应该会理解上面的面试题答案为什么是那样了。

猜你在找的Go相关文章