【go】defer底层原理

7 篇文章 0 订阅

defer的作用

defer声明的函数在当前函数return之后执行,通常用来做资源、连接的关闭和缓存的清除等。

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

注意:函数的return其实分为两部分:1将要返回的值赋值给返回值地址空间,2返回返回地址空间中的值。而defer发生在这两个操作中间,会造成一些defer不生效的错觉。详见go函数调用

defer原理

使用defer关键字声明的方法,实际上编译器会转换为两个方法的调用:

  • runtime.deferproc:将defer函数注册到goroutine中
  • runtime.deferreturn:在外层函数的末尾添加,执行注册的defer

我们都知道注册了多个defer函数后,defer的执行顺序是倒序的,类似栈的后进先出特性,实际上go使用链表实现的defer,每次新注册的defer都是头插法注册到链表中:

// 移除了无关代码
func deferproc(fn func()) {
	//获取当前g
	gp := getg()
	//新建defer结构
	d := newdefer()
	//将新创建的d添加到defer链表头
	d.link = gp._defer
	gp._defer = d
	d.fn = fn
}
// defer结构体中有指向下一个defer的指针
type _defer struct {
	started bool	//defer是否开始执行
	heap    bool	//是否堆分配
	openDefer bool	//是否是open coded
	sp        uintptr // sp at time of defer
	pc        uintptr // pc at time of defer
	fn        func()  // can be nil for open-coded defers
	_panic    *_panic // 触发当前defer的panic
	link      *_defer // defer链表
	fd   unsafe.Pointer // funcdata for the function associated with the frame
	varp uintptr        // value of varp for the stack frame
	framepc uintptr
}
// g结构体持有defer链表的头指针
type g struct{
	_defer    *_defer
}
嵌套defer

嵌套的defer依然按照顺序来看:先注册A中的A1,然后defer注册B,B中的defer顺序为B1->B2,最后注册A2。现在defer链表中是这样的:A2->B2->B1->A1。

func A() {
	defer A1()
	defer B()
	defer A2()
}
func B() {
	defer B1()
	defer B2()
}
func A1() {
	fmt.Println("defer A1")
}
func A2() {
	fmt.Println("defer A2")
}
func B1() {
	fmt.Println("defer B1")
}
func B2() {
	fmt.Println("defer B2")
}
func main() {
	A()
}

//输出
//defer A2
//defer B2
//defer B1
//defer A1

defer与panic

GO语言使用error机制来表示程序异常,而panic一般用于表示严重的错误出现。当发生panic时,如果没有使用recover来捕捉panic,那么程序就会退出。我们在日常开发中一般不会使用panic,因为大多数时候我们都希望程序能够运行下去,而不是直接停止服务。

当发生panic时,panic之后的代码不会执行,但是会触发之前注册的defer:

func A() {
	defer A1()
	panic("panic A")
	defer A3()
}
func A1() {
	fmt.Println("defer A1")
}
func A3() {
	fmt.Println("defer A3")
}
func main() {
	A()
}
/** 输出结果
defer A1
panic: panic A
**/

可以看到发生panicA时,触发了A中已经注册的defer A1,但是A3还未注册,所以不会执行。在同一个方法里只会有一个panic生效,因为后续代码不会执行了。

panic结构体
type _panic struct {
	argp      unsafe.Pointer // 当前panic要执行的defer函数参数
	arg       any            // panic的参数
	link      *_panic        // link to earlier panic
	pc        uintptr        // where to return to in runtime if this panic is bypassed
	sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
	recovered bool           // panic是否被恢复
	aborted   bool           // panic是否终止
	goexit    bool
}
// g中也保存panic指针
type g struct{
	_panic    *_panic
}

当发生一个panic时,当前的panic会被加入g的panic链表。然后执行defer链表。

多个panic和defer嵌套
func A() {
	defer A1()
	defer A2()
	panic("panic A")
}
func A1() {
	fmt.Println("defer A1")
	panic("panic A1")
}
func A2() {
	fmt.Println("defer A2")
}
func main() {
	A()
}
/**
defer A2
defer A1
panic: panic A
        panic: panic A1
**/        

这段代码中的执行逻辑:

  1. 注册defer A1和A2,现在的defer链表是:A2->A1
  2. 触发panic A,将panic A加入panic链表头
  3. 执行defer链表,正常执行defer A2,然后将A2移出defer链表
  4. 执行defer A1,此时A1中又触发了panic,不能正常执行完成,将A1的_defer.started标记为true,并将_defer._panic记为panic A。此时defer链表中只有一个started为true的defer A1
  5. 将新触发的panic A1加入到panic链表头,然后去执行defer链表,发现A1的started为true,且_panic是panic A。将panic A的aborted设为true,标记为终止。同时移除defer A1。
  6. 打印panic信息,从链表尾开始,panic A->panic A1

有recover的情况

recover的工作只是将panic的recovered字段设置为true。
先看一个简单的例子:

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

/**
输出结果
panic A
defer A2
defer A1
**/

这段代码的执行逻辑:

  1. 注册defer A1和A2
  2. 运行到panic时,将panic A加入panic链表。然后执行defer链表,先执行A2
  3. 执行A2时触发了recover,将panic A的recovered字段设为true,执行recover分支中的打印"panic A",然后继续执行A2中的打印语句"defer A2"
  4. 在defer A2执行完成后,go会检查当前panic已经recovered了,于是将panic A移出panic链表,然后通过_defer.sp和_defer.pc两个指针来找到要跳转的指令,最后将defer A2移出链表。
  5. 根据sp和pc跳回到defer执行逻辑,继续执行defer A1,打印出"defer A1"
多个panic
func A() {
	defer A2()
	defer A1()
	panic("panic A")
}
func A1() {
	fmt.Println("defer A1")
	panic("panic A1")
}
func A2() {
	if err := recover(); err != nil {
		fmt.Println(err)
	}
	fmt.Println("defer A2")
}
func main() {
	A()
}
/**
输出:
defer A1
panic A1
defer A2
**/

上面的代码在defer A1中也抛出了panic,多个panic同时存在时,recover只会捕捉到最新的。也只需要捕获一次。

recover之后又发生了panic
func A() {
	defer A2()
	defer A1()
	panic("panic A")
}
func A1() {
	fmt.Println("defer A1")
}
func A2() {
	if err := recover(); err != nil {
		fmt.Println(err)
	}
	fmt.Println("defer A2")
	panic("panic A1")
}
func main() {
	A()
}
/**
输出:
defer A1
panic A
defer A2
panic: panic A [recovered]
        panic: panic A1
**/

先触发的panic A被recover捕获到,打印的时候会标识为[recovered],再次触发的panic A1由于后续没有recover处理,所以程序终止。

defer的逐步优化

1.12版本中的defer是在堆中分配的,虽然golang实现了defer池可以复用defer结构体,但是效率依然不理想。在1.13版本中加入了defer栈上分配的功能,这样defer的性能就快很多。

func deferprocStack(d *_defer) {
	gp := getg()
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}
	// fn is already set.
	// The other fields are junk on entry to deferprocStack and
	// are initialized here.
	d.started = false
	d.heap = false
	d.openDefer = false
	d.sp = getcallersp()
	d.pc = getcallerpc()
	d.framepc = 0
	d.varp = 0
	// The lines below implement:
	//   d.panic = nil
	//   d.fd = nil
	//   d.link = gp._defer
	//   gp._defer = d
	// But without write barriers. The first three are writes to
	// the stack so they don't need a write barrier, and furthermore
	// are to uninitialized memory, so they must not use a write barrier.
	// The fourth write does not require a write barrier because we
	// explicitly mark all the defer structures, so we don't need to
	// keep track of pointers to them with a write barrier.
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}

1.14中加入了open coded defer,这使得我们在使用defer时不一定要创建defer结构体并加入到defer链表中,而是直接在编译阶段在函数体内加上相应的逻辑。

参考B站幼麟实验室

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值