在深入defer(续)文章中我们列举了两个例子来分析defer与return的执行顺序,但是最终的结论是两种方案都说的通。
可是真正被编译后的代码,只能有一种方案是正确的,所以本片文章中我们将两个例子分别反汇,看看它们的区别是什么,再决定到底是谁先执行。
defer在return之后执行实例的反汇编
请看汇编代码上的注释
"".test STEXT size=362 args=0x8 locals=0xb0 funcid=0x0
0x0000 00000 (main.go:11) TEXT "".test(SB), ABIInternal, $176-8
...
# 第13行log.Println(&a)
0x00e3 00227 (main.go:13) CALL log.Println(SB)
# 第14行defer定义的func方法
0x00e8 00232 (main.go:14) MOVL $8, ""..autotmp_6+32(SP)
0x00f0 00240 (main.go:14) LEAQ "".test.func1·f(SB), AX
0x00f7 00247 (main.go:14) MOVQ AX, ""..autotmp_6+56(SP)
0x00fc 00252 (main.go:14) LEAQ "".a+24(SP), AX
0x0101 00257 (main.go:14) MOVQ AX, ""..autotmp_6+104(SP)
0x0106 00262 (main.go:14) LEAQ ""..autotmp_6+32(SP), AX
0x010b 00267 (main.go:14) MOVQ AX, (SP)
# 将defer定义的func放入defer栈中,代码编译后匿名的func会被编译为test.func1方法,可以自行生成完整的汇编代码查看
0x010f 00271 (main.go:14) CALL runtime.deferprocStack(SB)
# 判断AX,AX 是否相等
0x0114 00276 (main.go:14) TESTL AX, AX
# 如果相等就执行317对应的指令
0x0116 00278 (main.go:14) JNE 317
# 执行282的指令
0x0118 00280 (main.go:14) JMP 282
# 这里第18行简单的return被编译成了如下一堆指令,从指令中可以看到
# 将a对应的值放在AX上
0x011a 00282 (main.go:18) MOVQ "".a+24(SP), AX
# 将AX的值放在对应的某个内存地址上,但一定不是a的内存地址
# 起码这里说明了除了a之外还有一个~r0这样的临时变量,真证返回的就是这临时变量了
0x011f 00287 (main.go:18) MOVQ AX, "".~r0+184(SP)
0x0127 00295 (main.go:18) XCHGL AX, AX
# 因为a与~r0指向两个内存地址,这也就说明defer栈的调用不会更改这个~r0的值,因为defer中使用的是a变量
0x0128 00296 (main.go:18) CALL runtime.deferreturn(SB)
0x012d 00301 (main.go:18) MOVQ 168(SP), BP
0x0135 00309 (main.go:18) ADDQ $176, SP
0x013c 00316 (main.go:18) RET
0x013d 00317 (main.go:14) XCHGL AX, AX
0x013e 00318 (main.go:14) NOP
0x0140 00320 (main.go:14) CALL runtime.deferreturn(SB)
0x0145 00325 (main.go:14) MOVQ 168(SP), BP
0x014d 00333 (main.go:14) ADDQ $176, SP
0x0154 00340 (main.go:14) RET
...
0x0165 00357 (main.go:11) JMP 0
defer在return之前执行实例的反汇编
下面代码断中我们只关心指令一与指令二的区别,其实没有任何区别,只是少了一个NOP指令,NOP指令翻译为什么都不做,所以下面两段指令无区别。
"".test STEXT size=351 args=0x8 locals=0xa8 funcid=0x0
0x0000 00000 (main.go:11) TEXT "".test(SB), ABIInternal, $168-8
...
0x00e3 00227 (main.go:13) CALL log.Println(SB)
0x00e8 00232 (main.go:14) MOVL $8, ""..autotmp_5+24(SP)
0x00f0 00240 (main.go:14) LEAQ "".test.func1·f(SB), AX
0x00f7 00247 (main.go:14) MOVQ AX, ""..autotmp_5+48(SP)
0x00fc 00252 (main.go:14) LEAQ "".a+176(SP), AX
0x0104 00260 (main.go:14) MOVQ AX, ""..autotmp_5+96(SP)
0x0109 00265 (main.go:14) LEAQ ""..autotmp_5+24(SP), AX
0x010e 00270 (main.go:14) MOVQ AX, (SP)
0x0112 00274 (main.go:14) CALL runtime.deferprocStack(SB)
0x0117 00279 (main.go:14) TESTL AX, AX
0x0119 00281 (main.go:14) JNE 309
0x011b 00283 (main.go:14) JMP 285
# 指令一
0x011d 00285 (main.go:18) XCHGL AX, AX
0x011e 00286 (main.go:18) NOP
0x0120 00288 (main.go:18) CALL runtime.deferreturn(SB)
0x0125 00293 (main.go:18) MOVQ 160(SP), BP
0x012d 00301 (main.go:18) ADDQ $168, SP
0x0134 00308 (main.go:18) RET
# 指令二
0x0135 00309 (main.go:14) XCHGL AX, AX
0x0136 00310 (main.go:14) CALL runtime.deferreturn(SB)
0x013b 00315 (main.go:14) MOVQ 160(SP), BP
0x0143 00323 (main.go:14) ADDQ $168, SP
0x014a 00330 (main.go:14) RET
...
0x015a 00346 (main.go:11) JMP 0
找出它们的区别并分析
# defer在return之后执行
0x011a 00282 (main.go:18) MOVQ "".a+24(SP), AX
0x011f 00287 (main.go:18) MOVQ AX, "".~r0+184(SP)
0x0127 00295 (main.go:18) XCHGL AX, AX
0x0128 00296 (main.go:18) CALL runtime.deferreturn(SB)
0x012d 00301 (main.go:18) MOVQ 168(SP), BP
0x0135 00309 (main.go:18) ADDQ $176, SP
0x013c 00316 (main.go:18) RET
# defer在return之前执行
0x011d 00285 (main.go:18) XCHGL AX, AX
0x011e 00286 (main.go:18) NOP
0x0120 00288 (main.go:18) CALL runtime.deferreturn(SB)
0x0125 00293 (main.go:18) MOVQ 160(SP), BP
0x012d 00301 (main.go:18) ADDQ $168, SP
0x0134 00308 (main.go:18) RET
区别
- 首先,我们可以确定两种方式生成的汇编指令有区别,如上
- 其次,第一个指令中自动生成了~r0变量,但第二个指令中没有生成临时变量
- 最后,return语句确实会被翻译成RET指令吗(我认为会)?如果会那么return一定在defer后执行的,但是return后面的代码却是在defer之前执行的
分析
两段代码执行后得出两种不同的结论,但是通过反汇编后发现,其实并不是两种结论,只是匿名返回值与具名返回值在编译器看来要进行不同的处理。
匿名返回值其实是下面这样的
func test() int{
a:=1
defer func(){a=2}
return a
}
# 等价于下面这样
func test()(b int){
a:=1
defer func(){
a=2
}
# 重点
b=a
return
}
谈谈我的理解
当我在func中返回数据时,因为func中的函数都是局部变量,局部变量在func执行结束后会被销毁,也就导致了func的调用者无法拿到返回值,为了解决这个问题,必须存在一个func之外的变量也就是作用域在func之外,但能被func使用的,所以编译器会取分析并确定需要创建这个变量,所以就出现了一个源代码中为定义,但确实存在的变量,这样我们之前的所有推论就都能解释。
从汇编层来说,在函数返回时只需要将返回值压栈,调用者去栈中取,栈顶的数据一定就是那个返回值。
当使用匿名返回值时,go生成的汇编指令中存在临时变量,就会导致栈顶的数据变化:
本来栈顶是a,但是在return时又创建了临时变量b,此时栈顶为b,然后defer栈对应函数执行完成后局部变量都会被销毁,回到defer栈的宿主函数时,栈顶还是b。如果defer对应的函数再次返回a时栈就变成了这样:a->b->a,可惜的是defer定义的函数默认调用者并不接收返回值,所以栈顶不可能是返回值a。
defer函数带返回值
package main
import (
"log"
)
func main() {
c := test()
log.Println("main", c)
}
func test() int {
a:=1
log.Println(a)
# 卵用没有,返回值并不会被接收
defer func() int {
a = 2
log.Println("defer", a)
return a
}()
return a
}
总结
defer与return的执行顺序是固定的,且只有一个答案:retrun表达式->defer->return(RTE指令),之前我们探讨两种情况的例子,其实是不了解匿名返回值与具名返回值的区别才导致的。
所以结论只有一个,记住它就行了。