go中defer/panic/recover的纠葛

1. defer

1.1 作用与应用场景

在函数调用结束后,完成一些收尾操作,例如数据库回滚、关闭文件、关闭数据库链接等等

1.2 基本原则

  1. defer函数参数会被预计算

  2. 多个defer执行顺序是先入后出的

  3. defer中可以改变命名返回变量的值

1.3 原理

golang中defer的实际结构体如下:

type _defer struct {
	siz       int32         //参数和结果的内存大小
	started   bool
	openDefer bool          //当前 defer 是否经过开放编码的优化
	sp        uintptr       //栈指针
	pc        uintptr       //调用方的程序计数器
	fn        *funcval      //defer 关键字中传入的函数
	_panic    *_panic       //触发延迟调用的结构体,可能为空
	link      *_defer       //将_defer结构体串联成一个链表
}

golang运行时有两个重要的defer机制入口:

  • runtime.deferproc 负责创建新的延迟调用,go编译器会将defer自动转为该方法

  • runtime.deferreturn 负责在函数调用结束时执行所有的延迟调用,go编译器会在所有调用defer的函数末尾添加上该方法

golang为每一个goroutine维护了一个延迟调度队列,即_defer链表。

在runtime.deferproc方法中,会构建一个新的runtime._defer结构体,设置fn、sp、pc等参数,并完成以下操作:

  1. 拷贝defer函数的参数到相邻空间,由此可知,defer函数的参数是在执行到defer语句时计算的,而不是在函数结束后调用defer时计算的。

    func deferproc(siz int32, fn *funcval) {
        gp := getg()
        ...
        sp := getcallersp()
    	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
    	callerpc := getcallerpc()
        
        // the arguments of fn are in a perilous state. The stack map
    	// for deferproc does not describe them. So we can't let garbage
    	// collection or stack copying trigger until we've copied them out
    	// to somewhere safe. The memmove below does that.
    	// Until the copy completes, we can only call nosplit routines.
        // 为了防止fn的参数被垃圾回收,或者栈拷贝(?),需要提前将其拷贝到安全的位置
        d := newdefer(siz)
    	if d._panic != nil {
    		throw("deferproc: d.panic != nil after newdefer")
    	}
    	d.link = gp._defer
    	gp._defer = d
    	d.fn = fn
    	d.pc = callerpc
    	d.sp = sp
    	switch siz {
    	case 0:
    		// Do nothing.
    	case sys.PtrSize:
    		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    	default:
    		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
    	}
        return0()
    }
    
  2. 将当前_defer结构体添加到goroutine的_defer链表最前面,而_defer的执行顺序是从前往后的,因此程序中后调用的defer函数先执行。

    func deferproc(siz int32, fn *funcval) {
        gp := getg()
        ...
        d := newdefer(siz)
        ...
    	d.link = gp._defer
    	gp._defer = d
    	...
    }
    

2. panic与recover

2.1 应用场景

程序遇到panic后,会立刻停止后续程序的执行,并进入当前goroutine的延迟调用队列,递归执行defer

recover是一个只能在defer中发挥作用的函数,可以终止由panic导致的程序崩溃

2.2 基本现象与原理

在go源码中,panic是用结构体runtime._panic表示的:

type _panic struct {
	argp      unsafe.Pointer  
	arg       interface{}     //panic函数传入的参数
	link      *_panic         //指向更早调用的_panic结构
	recovered bool            //表示当前_panic是否正确调用了recover恢复
	aborted   bool            //表示当前panic是否被终止
	pc        uintptr
	sp        unsafe.Pointer
	goexit    bool
}

2.2.1 程序崩溃

go编译器会将panic关键字转换为runtime.gopanic函数,在该函数下执行:

  1. 创建新runtime._panic结构,将自己添加到goroutine的_panic链表最前面
  2. 获取当前goroutine的_defer链表,依次循环执行
  3. 执行完所有的defer之后,调用runtime.fatalpanic函数,打印当前goroutine下_panic链表中所有_panic的入参信息【采用递归方法从后向前打印,由于采用头插法插入,故输出效果为先入先出】,最后终止当前进程(注意不是线程、也不是协程,而是进程被终止)
func gopanic(e interface{}) {
    gp := getg()
    ...
    //第一步, 创建新runtime._panic,将自己添加到goroutine的_panic链表最前面
    var p _panic
	p.arg = e
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    ...
    //第二步,循环执行所有defer
    for {
		d := gp._defer
		if d == nil {
			break
		}

		//嵌套panic的处理
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			...
		}
		d.started = true
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		done := true
		if d.openDefer {
			...
		} else {
             //执行defer函数
			p.argp = unsafe.Pointer(getargp(0))
			reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		}
		p.argp = nil
		...
		if p.recovered {
			//真正执行程序恢复的地方
             ...
		}
	}
	//第三步,打印panic信息并终止进程
	preprintpanics(gp._panic)
	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}

2.2.3 程序恢复

recover关键字会在编译时转换成runtime.gorecover方法,会将goroutine的首个_panic的recovered字段置为true,在runtime.gopanic函数中每次执行完一个_defer函数后会判断该字段,若为true,才执行真正的恢复过程,即使用runtime.gogo方法返回到defer调用recover方法的位置,继续defer链的执行

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}
func gopanic(e interface{}) {
    ...
    for {
		d := gp._defer
		if d == nil {
			break
		}
		...
		if p.recovered {
			gp._panic = p.link
			if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
				// A normal recover would bypass/abort the Goexit.  Instead,
				// we return to the processing loop of the Goexit.
				gp.sigcode0 = uintptr(gp._panic.sp)
				gp.sigcode1 = uintptr(gp._panic.pc)
				mcall(recovery)
				throw("bypassed recovery failed") // mcall should not return
			}
			...
		}
	}
    ...
}

3. 常见问题

3.1 为什么recover函数要包裹在defer的func()中?

//以下是一种无效调用recover()的例子:
func main(){
    defer recover()   //无效
    panic(1)
}
//正确使用recover()的例子:
func main(){
    defer func() {
                if err := recover(); err != nil {
                        fmt.Println(err)
                }
        }()
    panic(1)
}

这是因为recover()会被编译器转换为gorecover()函数:

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

当程序遇到defer时,defer函数的参数会预先计算并拷贝,故传入的参数argp会与实际执行gorecover时的p.argp不一致,导致程序直接返回nil,故无法在runtime.gopanic函数中正确地恢复程序。

因此,需要使用内联函数的方式才能正确地调用recover()恢复panic。

3.2 触发panic的协程是如何停止所有协程的?

panic在编译后执行runtime.gopanic函数,执行完defer链之后,若未执行recover(),则会执行runtime.fatalpanic函数,其中调用了exit(2),退出进程,从而其他的goroutine都会同时被终止.

func fatalpanic(msgs *_panic) {
	...
	systemstack(func() {
		exit(2)		//终止当前进程
	})

	*(*int)(nil) = 0 // not reached
}

3.3 panic触发之后defer函数里再次触发新的panic,会发生什么?

分几种情况:

(1)panic被recover捕捉恢复

func main() {
        defer func() {
                if err := recover(); err != nil {
                        fmt.Println(err)
                }
        }()
        defer func() {
                panic("panic3")
        }()
        defer func() {
                panic("panic2")
        }()
        panic("panic1")
}
//输出: panic3

(2)panic没有被recover

func main() {
	defer fmt.Println("in main")
	defer func() {
		defer func() {
			panic("panic again and again")
		}()
		panic("panic again")
	}()

	panic("panic once")
}
/*
输出:
in main
panic: panic once
	panic: panic again
	panic: panic again and again

goroutine 1 [running]:
*/

还有一种情况,panic被recover之后,后续的defer中又出现一个panic

func main() {
	defer func() {
		panic("panic3")
	}()
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	defer func() {
		panic("panic2")
	}()
	panic("panic1")
}
/*
输出:
panic2
panic: panic3

goroutine 1 [running]:
...
exit status 2
*/

解释以上现象涉及两个知识点:

(1) panic被defer中的recover函数恢复之后,程序会继续defer链的执行

(2) 嵌套panic的时候,会重复调用某个defer,defer上会标记started为true,表示已经被调用过,此时抛弃之前的panic,因此后续再进行Recover时,只能拿到最新的panic信息

if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			...
}
// 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

参考文章

[1] https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover

[2] https://blog.csdn.net/pengpengzhou/article/details/107663338

posted @ 2021-08-16 14:47  lwjj  阅读(190)  评论(0编辑  收藏  举报