Golang脱俗的defer

本文探讨了Go语言中defer的使用和实现原理,通过1.13和1.14版本的反汇编对比,揭示了defer从堆上分配到栈上分配的变化。文章详细介绍了defer如何将函数添加到goroutine的defer链表,并在函数结束或panic时按LIFO顺序执行。此外,还展示了deferproc函数的角色以及它如何处理函数参数。通过对代码的分析,读者可以更深入理解Go语言中defer的工作机制和优化。
摘要由CSDN通过智能技术生成

Golang脱俗的defer

  • 浅谈defer

GO中的defer一直都是项目中常用的依赖。无论是解锁,还是关闭文件,或者关闭session,大多都离不开defer。稍微使用过golang的粉(huan)丝(zhe)应该都知道,程序即使panic,defer也依然会照常执行。但是与传统中逐行执行相比,defer会造成较大的开销,这也是被行业中所诟病的痛点。相较于讲how,我们不如谈谈why吧,相比于语法,我觉得我们大可讨论下实现原理,来增加些“驾驶乐趣”。

说之前,我们简要的看下defer的实现原理哈~。先上一段代码吧:

package main

func main() {
    defer func() {
		print("hello, I'm xyp")
	}()
	return
}

我们分别使用两个版本的Go进行编译(1.13和1.14)。

使用golang的反汇编工具进行反汇编:

go tool compile -S main.go

不处所料,汇编代码中对defer的引用以及return都有所不同。

1.13反汇编后的结果

        0x0036 00054 (main.go:7)        MOVL    $0, (SP)
        0x003d 00061 (main.go:7)        LEAQ    "".main.func1·f(SB), AX
        0x0044 00068 (main.go:7)        MOVQ    AX, 8(SP)
        0x0049 00073 (main.go:7)        PCDATA  $1, $0
        0x0049 00073 (main.go:7)        CALL    runtime.deferproc(SB)
        0x004e 00078 (main.go:7)        TESTL   AX, AX
        0x0050 00080 (main.go:7)        JNE     84
        0x0052 00082 (main.go:7)        JMP     34
        0x0054 00084 (main.go:7)        XCHGL   AX, AX
 

 

1.14反汇编后的结果

        0x004f 00079 (main.go:6)        PCDATA  $1, $-1
        0x004f 00079 (main.go:6)        PCDATA  $0, $-2
        0x004f 00079 (main.go:6)        CALL    runtime.morestack_noctxt(SB)
        0x0054 00084 (main.go:6)        PCDATA  $0, $-1
        0x0054 00084 (main.go:6)        JMP     0

由此可见,1.13及之前的版本,defer都是在堆上分配,而1.14及之后的版本,defer分配在栈上。

  • 再探究竟

Go语言中,每个goroutine都有自己的一个defer链表,而runtime.deferproc函数做的事情就是把defer函数及其参数添加到链表中,即我们所谓的注册。然后编译器还会在当前函数结尾处插入runtime.deferreturn的调用代码,后者会按照LIFO的顺序调用当前函数注册的所有defer函数。如果当前goroutine发生了panic,或者调用了runtime.Goexit,runtime会按照LIFO的顺序遍历整个defer链表逐一执行defer函数,直到某个defer函数完成了recover,或者最后程序退出。

deferproc的函数原型如下:

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
	gp := getg()
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}

	// 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.
	sp := getcallersp()
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	callerpc := getcallerpc()

	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))
	}

	// deferproc returns 0 normally.
	// a deferred func that stops a panic
	// makes the deferproc return 1.
	// the code the compiler generates always
	// checks the return value and jumps to the
	// end of the function if deferproc returns != 0.
	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}

该function有两个input, siz即为编译器自动添加, 而fn指向一个runtime.funcval结构,里面有defer函数的地址

假设一段code如下:

func f1() {
	defer func() {
		print("hello world")
	}()
	print("hello xyp")
}

编译器会将其转为伪代码大致如下

func f1() {
	r := runtime.deferproc(16, f1_func1, 10, 20) // recover后,返回时r为1,否则为0
	if r > 0 {
		goto ret
	}
	print("hello xyp")
	runtime.deferreturn()
	return
ret:
	runtime.deferreturn()
}

// f1的defer函数
func f1_func1() {
	print("hello world")
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值