【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(
前言
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。如果大家有其他更好的使用建议,可以交流一下。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)