The realization principle of panic and recover in Golang

Today we talk about panic exceptions in golang and the capture of exceptions by recover. Since panic, recover, and defer are very close, we will explain them together today. There will be some defer knowledge involved. If you are interested, please see me Another article about defer is the implementation principle of defer in Golang .

Panic abnormal

Go's type system catches many errors at compile time, but some errors can only be checked at runtime, such as array access out of bounds, null pointer references, etc. These runtime errors can cause painc exceptions.
Generally speaking, when a panic exception occurs, the program will interrupt the operation and immediately execute the function delayed in the goroutine (defer mechanism). Subsequently, the program crashed and output log information.
Not all panic exceptions come from the runtime, directly calling the built-in panic function will also cause a panic exception.
Next, we try to find the underlying implementation of the built-in function panic() through its assembly code.

Note: I will comment out the function of each method in the source code, you can refer to the comments for understanding.

Write a simple code first and save it in the panic.go file

func main() {
    
    
	panic("err")
}

Then use the following command to compile the code:

go tool compile -S panic.go
 		0x0024 00036 (panic.go:10)      PCDATA  $2, $1
        0x0024 00036 (panic.go:10)      PCDATA  $0, $0
        0x0024 00036 (panic.go:10)      LEAQ    type.string(SB), AX
        0x002b 00043 (panic.go:10)      PCDATA  $2, $0
        0x002b 00043 (panic.go:10)      MOVQ    AX, (SP)
        0x002f 00047 (panic.go:10)      PCDATA  $2, $1
        0x002f 00047 (panic.go:10)      LEAQ    "".statictmp_0(SB), AX
        0x0036 00054 (panic.go:10)      PCDATA  $2, $0
        0x0036 00054 (panic.go:10)      MOVQ    AX, 8(SP)
        0x003b 00059 (panic.go:10)      CALL    runtime.gopanic(SB)

We can see that the panic() function call has been replaced with the runtime.gopanic() function
. Before looking at the function, let’s take a look at the structure of panic.

runtime\runtime2.go:_panic

type _panic struct {
    
    
	argp      unsafe.Pointer // 指向在panic下运行的defer的参数的指针
	arg       interface{
    
    }    // panic的参数
	link      *_panic        // 链接到更早的panic,新panic添加到表头
	recovered bool           // 该panic是否被recover
	aborted   bool           // 该panic是否强制退出
}

Next, let’s analyze the runtime.gopanic() function

runtime\panic.go

func gopanic(e interface{
    
    }) {
    
    
	//获取当前goroutine
	gp := getg()
	...
	//生成一个新的panic结构
	var p _panic
	p.arg = e
	//指向更早的panic
	p.link = gp._panic
	//绑定到goroutine
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	atomic.Xadd(&runningPanicDefers, 1)

	//循环goroutine中的defer链表
	for {
    
    
		d := gp._defer
		if d == nil {
    
    
			break
		}
		//如果defer已经被调用
		//如果该defer已经由较早的panic或者Goexit使用(表示引发了新的panic)
		//则从链表中去除这个panic,之前的panic或Goexit将不会继续运行。
		if d.started {
    
    
			if d._panic != nil {
    
    
				d._panic.aborted = true
			}
			d._panic = nil
			d.fn = nil
			gp._defer = d.link
			//释放该defer
			freedefer(d)
			//跳过循环,继续下一个defer
			continue
		}
		// 将defer标记已调用,但保留在列表中
		//这样 traceback 在栈增长或者 GC 的时候,能够找到并更新 defer 的参数栈帧
        // 并用 reflectcall 执行 d.fn
		d.started = true
		//记录在 defer 中发生的 panic
		//如果在 defer 的函数调用过程中又发生了新的 panic,那个 panic 会在链表中找到 d
        // 然后标记 d._panic(指向当前的 panic) 为 aborted 状态。
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		p.argp = unsafe.Pointer(getargp(0))
		//执行defer后面的fn,如果有recover()函数会执行recover
		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		p.argp = nil

		// reflectcall 并没有 panic,移除 d
		if gp._defer != d {
    
    
			throw("bad defer entry in panic")
		}
		//清空defer
		d._panic = nil
		d.fn = nil
		//下一个defer
		gp._defer = d.link

		// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
		//GC()
		
		//defer语句下一条语句的地址
		pc := d.pc
		//获取rsp寄存器的值的指针
		//必须是指针,以便在堆栈复制期间进行调整
		sp := unsafe.Pointer(d.sp) 
		//释放defer
		freedefer(d)
		//如果panic被recover
		//会在gorecove 函数中已经修改为 true ,等会我们在讲
		if p.recovered {
    
    
			//统计
			atomic.Xadd(&runningPanicDefers, -1)
			
			//下一个panic
			gp._panic = p.link
			// 已标记已中止的panic,q且保留在g.panic列表中。
			//从列表中删除它们。
			for gp._panic != nil && gp._panic.aborted {
    
    
				gp._panic = gp._panic.link
			}
			//处理完所有panic
			if gp._panic == nil {
    
     // 必须用信号完成
				gp.sig = 0
			}
			// Pass information about recovering frame to recovery.
			//将有关恢复帧的信息传递给recovery函数
			//通过之前传入的 sp 和 pc 恢复
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			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.
	preprintpanics(gp._panic)
	
	//致命错误,终止程序
	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}

Next, let’s take a look at how it is replied through the recovery function

func recovery(gp *g) {
    
    
	// Info about defer passed in G struct.
	sp := gp.sigcode0
	pc := gp.sigcode1

	// d's arguments need to be in the stack.
	if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
    
    
		print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
		throw("bad recovery")
	}
	//让这个 defer 结构体的 deferproc 位置的调用重新返回
    // 这次将返回值修改为 1
	gp.sched.sp = sp
	gp.sched.pc = pc
	gp.sched.lr = 0
	gp.sched.ret = 1
	//直接跳回到deferreturn那里去
	gogo(&gp.sched)
}

Let's summarize the whole process again:

  1. First create a _panic structure and load it into the header of the linked list
  2. Traverse the defer list of the current goroutine,
    • If defer is marked as called, jump out of the current loop and enter the next defer;
    • Otherwise, mark the current defer as called and execute the function behind defer at the same time. If there is recover, it will pass the address of the next assembly instruction (pc) of deferproc passed in when the defer was created before, and the top of the function call stack The position (sp) returns to the position of deferreturn, otherwise, exit the program directly

Recover catch exception

Generally speaking, we should not do any handling of panic exceptions, but sometimes, maybe we can recover from the exceptions, at least we can do some operations before the program crashes. For example: When a web server encounters an unexpected serious problem, all connections should be closed before it crashes. The server can even feed back abnormal information to the client to help debugging.
If the built-in function recover is called in the defer function, and a panic exception occurs in the function defining the defer statement, recover will make the program recover from the panic and return the panic value. The function that caused the panic exception will not continue to run, but it can return normally. Call recover when panic has not occurred, recover will return nil.

Use of recover function

1.recover must be used in conjunction with defer
func main() {
    
    
	defer func() {
    
    
			recover()
			 }()
	panic("err")
}

A situation similar to the following is not acceptable:

func main() {
    
    
	recover()
	panic("触发异常")

}

Insert picture description here

2. The recover must be called directly in the defer function, and cannot be encapsulated or nested
func main() {
    
    
	defer func() {
    
    
		if r := MyRecover(); r != nil {
    
    
			fmt.Println(r)
		}
	}()
	panic("err")
}
func MyRecover() interface{
    
    } {
    
    
	fmt.Println("recover")
	return recover()
}

Insert picture description here
Similarly, nesting in defer is not possible

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

If we call the MyRecover function directly in the defer statement, it will work again:

func main() {
    
    
	//正常捕获
	defer MyRecover()
	panic("err")
}
func MyRecover() interface{
    
    } {
    
    
	fmt.Println("recover")
	return recover()
}

However, if the defer statement directly calls the recover function, it still cannot catch the exception normally:

func main() {
    
     
	// 无法捕获异常
	defer recover()
	panic("err")
}

Only one stack frame must be separated from the stack frame with the exception before the recover function can catch the exception normally. In other words, the recover function catches the exception of the stack frame of the grandfather's first-level calling function (just can span a layer of defer function)!

At the same time, in order to avoid the indiscriminate panic being restored, which may cause system vulnerabilities, the safest way to cook is to deal with different types of errors separately

Principle of recover function

Next, we use the underlying source code to see how it achieves these restrictions:

runtime\panic.go

func gorecover(argp uintptr) interface{
    
    } {
    
    
	
	gp := getg()
	p := gp._panic
	//必须存在panic
	//非runtime.Goexit();
	//panic还未被恢复
	//argp == uintptr(p.argp)
	//p.argp是最顶层的延迟函数调用的参数指针,argp是调用recover函数的参数地址,通常是defer函数的参数地址
	//如果两者相等,说明可以被恢复,这也是为什么recover必须跟在defer后面且recover 函数捕获的是祖父一级调用函数栈帧的异常的原因
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
    
    
		//将recovered 标志改为true
		p.recovered = true
		return p.arg
	}
	return nil
}

The gorecover function is relatively simple, that is, set recovered to true, indicating that the function behind defer contains recover

to sum up

  • The recover function is in the defer function
  • The recover function is directly called by the defer function
  • If multiple defer functions are included, after the previous defer eliminates panic through recover(), the remaining defer in the function will still be executed, but it cannot be recovered() again
  • Call panic continuously, only the last one will be captured by recover

reference

  1. Go language bible .
  2. Advanced programming in Go language .

Guess you like

Origin blog.csdn.net/xzw12138/article/details/109256075