Golang Return Defer 流程分析

Golang Return Defer 流程分析

之前回答过一个关于defer没有正确赋值的问题 Golang中国-defer拿不到返回值,感觉当时回答时写的都是别人总结出的的东西,没有进行深度思考,为什么会这样也没有搞清楚,后来自己特地测试分析了一波,有不对的地方还请指正

代码测试

关于返回值遇到defer赋值有两种情况:

匿名返回值
package main

import "fmt"

func main() {
    fmt.Printf("x: %d\n", Parser())
}

func Parser() int {
    x := 1
    defer func() {
        x = 2
    }()
    return x
}

这种情况下,defer中对变量x的赋值会失败,程序会输出x: 1,原本设想的是会输出x: 2
出现这种情况的原因是,函数中return操作不是原子操作,在return阶段会执行三步操作:

  1. 返回值赋值。在这里因为返回值没有命名,所以进行了一个x赋值给匿名变量的操作
  2. 执行defer函数。执行x = 2,可以看到在第1步已经给返回值进行了赋值操作,所以这里x的改变不会影响到返回值。
  3. 函数返回。匿名变量的值返回给了调用者。
命名返回值
package main

import "fmt"

func main() {
    fmt.Printf("x: %d\n", Parser())
}

func Parser() (x int) {
    x = 1
    defer func() {
        x = 2
    }()
    return x
}

当使用命名返回值时,程序会输出预期中的x: 2
还是对return执行的过程进行分解:

  1. 返回值赋值。返回值变量已经在程序定义时进行了声明,在return时不需要再使用匿名变量,所以原有的第一步给匿名变量赋值已经不存在了。
  2. 执行defer函数。因为返回值使用的是命名变量,在这里为x进行赋值依旧会生效。
  3. 返回返回。命名变量x的值返回给了调用者。

汇编分析

通过分析汇编代码,能更加直观的理解Golang对return-defer执行的操作。

匿名返回值
    // 声明函数 Parser
    0x0000 00000 (test.go:7)    TEXT    "".Parser(SB), $40-8
    0x0000 00000 (test.go:7)    MOVQ    (TLS), CX
    0x0009 00009 (test.go:7)    CMPQ    SP, 16(CX)
    0x000d 00013 (test.go:7)    JLS    127
    // 分配堆栈空间
    0x000f 00015 (test.go:7)    SUBQ    $40, SP
    0x0013 00019 (test.go:7)    MOVQ    BP, 32(SP)
    0x0018 00024 (test.go:7)    LEAQ    32(SP), BP
    0x001d 00029 (test.go:7)    FUNCDATA    $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
    0x001d 00029 (test.go:7)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    // r0数据寄存器赋值为0 -- 匿名返回值的初始化
    0x001d 00029 (test.go:7)    MOVQ    $0, "".~r0+48(SP)
    // x变量赋值为1
    0x0026 00038 (test.go:8)    MOVQ    $1, "".x+24(SP)
    0x002f 00047 (test.go:11)    LEAQ    "".x+24(SP), AX
    0x0034 00052 (test.go:11)    MOVQ    AX, 16(SP)
    0x0039 00057 (test.go:9)    MOVL    $8, (SP)
    0x0040 00064 (test.go:9)    LEAQ    "".Parser.func1·f(SB), AX
    0x0047 00071 (test.go:9)    MOVQ    AX, 8(SP)
    0x004c 00076 (test.go:9)    PCDATA    $0, $0
    // 注册defer函数
    0x004c 00076 (test.go:9)    CALL    runtime.deferproc(SB)
    0x0051 00081 (test.go:9)    TESTL    AX, AX
    0x0053 00083 (test.go:9)    JNE    111

    // -- 以下是return部分操作
    // x赋值到r0寄存器 -- 匿名返回值的赋值操作
    0x0055 00085 (test.go:12)    MOVQ    "".x+24(SP), AX
    0x005a 00090 (test.go:12)    MOVQ    AX, "".~r0+48(SP)
    // GC进行的操作
    0x005f 00095 (test.go:12)    PCDATA    $0, $0
    0x005f 00095 (test.go:12)    XCHGL    AX, AX
    // 调用defer函数
    0x0060 00096 (test.go:12)    CALL    runtime.deferreturn(SB)
    0x0065 00101 (test.go:12)    MOVQ    32(SP), BP
    // 回收堆栈空间并返回
    0x006a 00106 (test.go:12)    ADDQ    $40, SP
    0x006e 00110 (test.go:12)    RET

因为汇编代码太长,这里只贴出Parser函数的汇编代码。
可以看到针对匿名返回值(r0)在程序开始时会初始化为默认值0,在函数结束Return时,分成了三步走的操作,赋值、defer、回收空间并返回。defer执行时,赋值操作x -> r0已经执行完毕,所以返回值没有命名时,defer操作中对返回值的赋值无效。

命名返回值
    // 声明函数Parser
    0x0000 00000 (test.go:9)    TEXT    "".Parser(SB), $32-8
    0x0000 00000 (test.go:9)    MOVQ    (TLS), CX
    0x0009 00009 (test.go:9)    CMPQ    SP, 16(CX)
    0x000d 00013 (test.go:9)    JLS    108
    // 分配堆栈空间
    0x000f 00015 (test.go:9)    SUBQ    $32, SP
    0x0013 00019 (test.go:9)    MOVQ    BP, 24(SP)
    0x0018 00024 (test.go:9)    LEAQ    24(SP), BP
    0x001d 00029 (test.go:9)    FUNCDATA    $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
    0x001d 00029 (test.go:9)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    // x赋值为1
    0x001d 00029 (test.go:10)    MOVQ    $1, "".x+40(SP)
    0x0026 00038 (test.go:13)    LEAQ    "".x+40(SP), AX
    0x002b 00043 (test.go:13)    MOVQ    AX, 16(SP)
    0x0030 00048 (test.go:11)    MOVL    $8, (SP)
    0x0037 00055 (test.go:11)    LEAQ    "".Parser.func1·f(SB), AX
    0x003e 00062 (test.go:11)    MOVQ    AX, 8(SP)
    0x0043 00067 (test.go:11)    PCDATA    $0, $0
    // 注册defer
    0x0043 00067 (test.go:11)    CALL    runtime.deferproc(SB)
    0x0048 00072 (test.go:11)    TESTL    AX, AX
    0x004a 00074 (test.go:11)    JNE    92
    0x004c 00076 (test.go:14)    PCDATA    $0, $0
    0x004c 00076 (test.go:14)    XCHGL    AX, AX
    // 调用defer
    0x004d 00077 (test.go:14)    CALL    runtime.deferreturn(SB)
    0x0052 00082 (test.go:14)    MOVQ    24(SP), BP
    // 回收堆栈空间并返回
    0x0057 00087 (test.go:14)    ADDQ    $32, SP
    0x005b 00091 (test.go:14)    RET

匿名返回值的汇编相比较,命名返回值的进行的操作有一些差异。

  1. 分配栈帧空间上,匿名的栈帧大小为$40(40字节),而命名的栈帧大小为$32(32字节),8字节空间的差异主要来自,命名返回值直接使用了x,没有重新声明局部变量x。
    栈帧空间的差异可以得到一个结论:命名返回值函数中使用的x就是实际的返回变量x,而匿名返回值函数中使用的x只是局部变量,实际返回的并不是x,而只是x赋给匿名变量(r0)的值。
  2. 在return执行过程中,命名返回值函数的return过程缺少了赋值操作,应该是因为在声明的同时已经经过初始化且函数中可以直接使用该变量,所以可以省略return返回时赋值的步骤。而匿名返回值在函数中无法直接使用,只可以在return时进行赋值。
  3. 因为1,2的差异,所以在使用命名返回值时,defer对返回值的影响可以直接体现出来,而在使用匿名返回值时,defer对x的二次赋值无法对匿名返回值产生影响。

因为查找到的与Golang汇编相关的资料很少,只能看懂一小部分代码,不当之处还请指正。

参考
https://lrita.github.io/2017/12/12/golang-asm/

http://blog.rootk.com/post/golang-asm.html

https://my.oschina.net/henrylee2cn/blog

本文使用CC BY-NA-SA 4.0协议许可
本文链接:http://404-notfound.com/golang-return-defer/