GO:延期声明是如何工作的?

2020-05-10 20:07:56

DEFER语句是在函数返回之前执行代码的一种便捷方式,如Golang规范中所述:

相反,延迟函数在周围函数返回之前立即调用,顺序与它们被延迟的顺序相反。

func main(){defer func(){println(`defer 1`)}()defer func(){println(`defer 2`)}()}延迟2<;-最后一个进来,第一个出去延迟1。

让我们先看一下内部结构,然后再看一个更复杂的案例。

GO运行时使用链表实现后进先出。实际上,DEFER结构有一个指向要执行的下一个结构的链接:

type_defer struct{siz int32已启动bool sp uintptr PC uintptr fn*funcval_Panic*_Panic link*_defer//要执行的下一个延迟函数}。

创建新的DEFER方法时,它将附加到当前的Goroutine,而前一个方法将链接到新的方法,作为下一个要执行的函数:

func newdefer(Siz Int32)*_defer{var d*_defer gp:=getg()//获取当前goroutine[.]//延迟列表现在附加到new_defer struct d.link=gp._defer gp._defer=d//新结构现在是第一个被调用的返回d}

函数延迟返回(Arg0 Uintptr){gp:=getg()//获取当前的goroutine d:=gp._defer//如果d==nil{//如果没有延迟函数,则只返回return}[.]。fn:=d.fn//获取要调用的函数d.fn=nil//重置函数gp._defer=d.link//将下一个函数附加到goroutine fredefer(D)//释放_defer strucc jmpdefer(fn,uintptr(unsafe.Pointer(&;arg0)//调用函数}。

正如我们所看到的,我们没有循环延迟函数,它是逐个出栈的。生成的ASM代码确认了此行为:

//第一延迟函数0x001d 00029(Main.Go:6)MOVL$0,(SP)0x0024 00036(Main.Go:6)PCDATA$2,$10x0024 00036(Main.Go:6)LEAQ";";.main.func1·f(SB),AX 0x002b 00043(main.go:6)PCDATA$2,$0x002b 00043(main.go:6)MOVQ AX,8(SP)0x0030 00048(main.go:6)调用运行时.deferproc(SB)0x0035 00053(main.go:6)TESTL AX,AX 0x0037 00055(main.go:6)JNE117/秒延迟函数0x.GO:6。.main.func2·f(SB),AX 0x0047 00071(Main.GO:10)PCDATA$2,$0x0047 00071(Main.GO:10)MOVQ AX,8(SP)0x004c 00076(Main.GO:10)调用运行时.deferproc(SB)0x0051 00081(Main.GO:10)TESTL AX,AX 0x0053 00083(Main.GO:10)JNE101//主函数0x0051 00081结束。BP 0x0060 00096(Main.GO:18)ADDQ$24,SP 0x0064 00100(Main.GO:18)RET 0x0065 00101(Main.GO:10)XCHGL AX,AX 0x0066 00102(Main.GO:10)调用运行时.deferreturn(SB)0x006b 00107(Main.GO:10)MOVQ 16(SP),BP 0x0070 00112(Main.GO:10)ADDQ$24,SP 0x0074 00116。

方法deferproc被调用两次,并且在内部调用我们前面看到的方法newdefer,以将我们的函数注册为延迟方法。然后,在函数结束时,由于deferreturn函数,将逐个调用延迟方法。

Go库确实向我们展示了struct_defer还链接到_Panic*_Panic属性。让我们通过另一个示例来看看它的用处。

延迟函数访问返回结果的唯一方法是使用规范中说明的命名结果参数:

如果延迟函数是函数文字,并且周围的函数具有在文字范围内的命名结果参数,则延迟函数可以在返回结果参数之前对其进行访问和修改。

func main(){fmt.Printf(";带命名参数,x:%d\n";,namedParam())fmt.Printf(";不带命名参数,x:%d\n";,notNamedParam())}func namedParam()(X Int){x=1 defer func(){x=2}()return x}func notNamedParam()(Int){x:=1带命名参数的defer func(){x=2}()return x},x:2,不带命名参数,x:1

一旦确认了此行为,我们就可以将其与恢复功能混合使用。事实上,正如“推迟、恐慌和恢复”博客文章中所解释的那样:

RECOVER是一个内置功能,可以重新控制惊慌失措的猩猩。RECOVER仅在延迟函数内有用。

正如我们已经看到的,_DEFER结构链接到在紧急调用期间链接的_PARGIC属性:

Func Gopanic(e界面{}){[.]。变量P_FARCH[.]。d:=gp._defer//goroutine上的当前附加延迟[.]。d._PARGIC=(*_PARGIC)(NOEASE(unsafe.Pointer(&;p)[.]}。

实际上,在调用延迟函数之前,会在出现恐慌的情况下调用gopanic方法:

0x0067 00103(main.go:21)调用runtime.gopanic(SB)0x006c 00108(main.go:21)UNDEF 0x006e 00110(main.go:16)xchgl ax,ax 0x006f 00111(main.go:16)调用runtime.deferreturn(SB)。

以下是Recover函数利用命名结果参数的示例:

func main(){fmt.Printf(";Error from err1:%v\n";,err1())fmt.Printf(";Error from err2:%v\n";,err2())}func err1()error{var err error defer func(){if r:=recover();r!=nil{err=errors.New(";Recovered";)}}()Panic(`foo`)return err}func err2()(Err Error){defer func(){if r:=recover();r!=nil{err=errors.New(";Recovered";)}}()Panic(`foo`)return err}Error from err1:<;nil>;error from err2:已恢复

这两者的结合允许我们正确地使用Recover函数和我们想要返回给调用者的错误。为了结束这篇关于延迟函数的文章,让我们来看看对它的改进。

改进了DEFER用法的最后一个版本是GO版本1.8。我们可以看到,在GO库中运行基准测试(在1.7到1.8之间):

名称旧时间/操作新时间/操作增量延迟-4 99.0 ns±9%52.4 ns±5%-47.04%(p=0.000 n=9+10)延迟10-4 90.6 ns±13%45.0 ns±3%-50.37%(p=0.000 n=10+10)。

这一改进要归功于这个CL,它改进了分配方式,避免了堆栈增长。

还有一个针对DEFER语句的优化,该语句没有跳过内存复制的参数。以下是带/不带参数的延迟函数的基准:

名称旧时间/操作新时间/操作增量延迟-4 51.3 ns±3%45.8 ns±1%-10.72%(p=0.000 n=10+10)