【Free Style】深入Go语言模型5:panic和recover异常处理机制底层实现分析

举报
霜雯 发表于 2017/11/09 11:26:19 2017/11/09
【摘要】 前言Go语言里面没有Java、C++里面那种try-catch结构化异常处理,而是提供了panic和recover来进行抛出错误和捕获错误的机制:func test() { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() panic(

前言

Go语言里面没有Java、C++里面那种try-catch结构化异常处理,而是提供了panic和recover来进行抛出错误和捕获错误的机制:

func test() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) 
        }
    }()

    panic("error!")
}

如果函数F发生panic,F中的defer函数仍然会执行,调用F的函数中的defer也会执行,直到goroutine上最上层函数。然后goroutine结束,并报告相应的错误。

panic的底层实现

我们先把前面的代码进行反汇编:

TEXT    "".test(SB), $56-0
LEAQ    "".test.func1·f(SB), AX   //重点关注
MOVQ    AX, 8(SP)
PCDATA    $0, $0
CALL    runtime.deferproc(SB)
TESTL    AX, AX
JNE    141
LEAQ    go.string."error!"(SB), AX
MOVQ    AX, ""..autotmp_0+32(SP)
MOVQ    $6, ""..autotmp_0+40(SP)
LEAQ    type.string(SB), AX
MOVQ    AX, (SP)
LEAQ    ""..autotmp_0+32(SP), AX
MOVQ    AX, 8(SP)
PCDATA    $0, $1
CALL    runtime.convT2E(SB)
MOVQ    16(SP), AX
MOVQ    24(SP), CX
MOVQ    AX, (SP)
MOVQ    CX, 8(SP)
PCDATA    $0, $1
CALL    runtime.gopanic(SB)  //重点关注
UNDEF
PCDATA    $0, $0
XCHGL    AX, AX
CALL    runtime.deferreturn(SB)
MOVQ    48(SP), BP
ADDQ    $56, SP
RET

上面这个是有一个"".test.func1,就是defer的匿名函数,我们再看看这个函数的实现:

TEXT    "".test.func1(SB), $72-0
LEAQ    ""..fp+80(FP), AX
MOVQ    AX, (SP)
PCDATA    $0, $0
CALL    runtime.gorecover(SB)  //重点关注
MOVQ    16(SP), AX
MOVQ    8(SP), CX
TESTQ    CX, CX
JNE    $0, 75
MOVQ    64(SP), BP
ADDQ    $72, SP
RET

我们从上面的代码可以看出panic主要是调用下面这个函数:
CALL runtime.gopanic(SB)

func gopanic(e interface{}) {
    gp := getg()

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

    //下面的代码和Goexit类似,调用所有的defer
    for {
        d := gp._defer
        if d == nil {
            break
        }

        // 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
        }

        // 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))
        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

        // 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里面有recover操作,就执行下面的处理
        if p.recovered {
            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

            //执行recover操作,不在返回
            mcall(recovery)
            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.

    //触发panic, 打印调用栈,调用_ExitProcess退出进程
    preprintpanics(gp._panic)
    startpanic()
    printpanics(gp._panic)
    dopanic(0)       // should not return
    *(*int)(nil) = 0 // not reached
}

recover的实现

func gorecover(argp uintptr) interface{} {
    // Must be in a function running as part of a deferred call during the panic.
    // Must be called from the topmost function of the call
    // (the function used in the defer statement).
    // p.argp is the argument pointer of that topmost deferred function call.
    // Compare against argp reported by caller.
    // If they match, the caller is the one who can   recover.
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

从上面可以看到这个函数仅仅是设置一个标志位:p.recovered = true
这个标志位在gopanic函数里面已经看到过:

if p.recovered {
    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)
    throw("recovery failed") // mcall should not return
}

按照调用顺序,panic触发以后,先调用defer函数,如果defer函数里面有recover操作,那么p.recovered就被设置成true,那么接下来就是上面gopanic中的代码逻辑,进行recovery操作了。

If recover is called outside the deferred function it will not stop a panicking sequence.
recover函数只能在defer函数中使用,否则调用总是返回nil。任何未捕获的错误都会沿调⽤用堆栈向外传递。

func recoverImpl() { recover() }

func test() {
    defer recover() // 无效调用!
    defer fmt.Println(recover()) // 无效调用!
    defer func() {
        func() {
            fmt.Println("defer inner")
            recover() // 无效调用!
        }()
    }()

    defer func() { recoverImpl() }()  //无效调用

    //下面这些就是有效调用
    defer func(){defer recover()}()

    var recover = func() { recover() }
    defer recover()

    defer recoverImpl()

    panic("test panic")
}

func main() {
    test()
}

为什么只有在defer的第一层调用recover才有用,这个其实前面gorecover函数已经说明了:
看下面的函数:
p.argp is the argument pointer of that topmost deferred function call.
只有调用defer函数调用recover的才匹配

func gorecover(argp uintptr) interface{} {
    // Must be in a function running as part of a deferred call during the panic.
    // Must be called from the topmost function of the call
    // (the function used in the defer statement).
    // p.argp is the argument pointer of that topmost deferred function call.
    // Compare against argp reported by caller.
    // If they match, the caller is the one who can   recover.
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

Go语言内置的panic错误流程

func test() {
    defer func() {
        if err := recover(); err != nil {
            buf := make([]byte, 1<<16)
            runtime.Stack(buf, false)
            fmt.Println(string(buf))
        }
    }()

    a := [2]int{0, 0}

    b := 10 / a[0]
    fmt.Println(b)
}

上面的错误会触发panic,这种是由Go语言runtime内置的错误,实现是通过OS的signal实现的。
我们先看看Go语言内置了哪些panic错误:

var indexError = error(errorString("index out of range"))

func panicindex() {
    panicCheckMalloc(indexError)
    panic(indexError)
}

var sliceError = error(errorString("slice bounds out of range"))

func panicslice() {
    panicCheckMalloc(sliceError)
    panic(sliceError)
}

var divideError = error(errorString("integer divide by zero"))

func panicdivide() {
    panicCheckMalloc(divideError)
    panic(divideError)
}

var overflowError = error(errorString("integer overflow"))

func panicoverflow() {
    panicCheckMalloc(overflowError)
    panic(overflowError)
}

var floatError = error(errorString("floating point error"))

func panicfloat() {
    panicCheckMalloc(floatError)
    panic(floatError)
}

var memoryError = error(errorString("invalid memory address or nil pointer dereference"))

func panicmem() {
    panicCheckMalloc(memoryError)
    panic(memoryError)
}

上面一共有6中错误,包括数组越界,切片越界,除0操作,整数溢出,浮点数错误,内存错误或者空指针,这六种错误go语言在运行时会进行检查,并且在适当的时候出发panic。

func sigpanic() {
    g := getg()
    if !can  panic(g) {
        throw("unexpected signal during runtime execution")
    }

    switch g.sig {
    case _EXCEPTION_ACCESS_VIOLATION:
        if g.sigcode1 < 0x1000 || g.paniconfault {
            panicmem()
        }
        print("unexpected fault address ", hex(g.sigcode1), "\n")
        throw("fault")
    case _EXCEPTION_INT_DIVIDE_BY_ZERO:
        panicdivide()
    case _EXCEPTION_INT_OVERFLOW:
        panicoverflow()
    case _EXCEPTION_FLT_DENORMAL_OPERAND,
        _EXCEPTION_FLT_DIVIDE_BY_ZERO,
        _EXCEPTION_FLT_INEXACT_RESULT,
        _EXCEPTION_FLT_OVERFLOW,
        _EXCEPTION_FLT_UNDERFLOW:
        panicfloat()
    }
    throw("fault")
}

panic和recover的推荐使用

在Go语言有error错误,并且支持多返回值,那么什么时候使用panic,什么时候使用error呢?

  1. 如果是业务流程上面出现了不可修复性的错误,那么使用panic。

  2. panic只能在一个包里面使用,不要随便扩散到包外边,包和包之间建议error

其他情况建议使用error。如果大家有其他更好的使用建议,可以交流一下。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。