【Free Style】深入Go语言模型5:panic和recover异常处理机制底层实现分析
前言
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呢?
如果是业务流程上面出现了不可修复性的错误,那么使用panic。
panic只能在一个包里面使用,不要随便扩散到包外边,包和包之间建议error
其他情况建议使用error。如果大家有其他更好的使用建议,可以交流一下。
- 点赞
- 收藏
- 关注作者
评论(0)