GO成神之路:defer与return到底谁先执行?|Go主题月

1,289 阅读6分钟

深入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

区别

  1. 首先,我们可以确定两种方式生成的汇编指令有区别,如上
  2. 其次,第一个指令中自动生成了~r0变量,但第二个指令中没有生成临时变量
  3. 最后,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指令),之前我们探讨两种情况的例子,其实是不了解匿名返回值与具名返回值的区别才导致的。

所以结论只有一个,记住它就行了。