golang defer陷阱

golang defer陷阱
[TOC]

测验

下面程序的输出是什么?

package main



//!+f
func main() {
    println(f1())
    println(f2())
    println(f3())

}

func f1() (r int) {
    defer func() {
        r++
    }()
    return 0
}

func f2() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t
}

func f3() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1
}

答案

如果你的结果是

1
5
1

恭喜你,答对了。这篇文章您不必看了。

如果答错了。请继续阅读

一般解答

预备知识

  1. 函数调用方负责开辟栈空间,包括形参和返回值的空间。
  2. 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。

f1

defer语句后面的匿名函数是对函数返回值r的闭包引用,f1函数的逻辑如下:

  1. r是函数的有名返回值,被分配在栈上,其地址又被称为返回值所在栈区。首先r被初始化为0.
  2. “return 0″会复制0 到返回值栈区,返回值r被赋值为0。
  3. 执行defer语句,由于匿名函数对返回值r是闭包引用,所以r++执行后,函数返回值被修改为1。
  4. defer语句执行完后RET返回,此时函数的返回值仍为1。

图解如下

f1指令序列 内存值
r=0 r=0
copy 0 to r r=0
defer r++ r=1
RET r=1

f2

f2函数的逻辑如下:

  1. 首先r被初始化为0.
  2. 引入局部变量t, 并初始化为5.
  3. 复制t的值5到返回值r所在的栈区。
  4. 执行defer语句,由于匿名函数对t是闭包引用,所以t=t+5执行后,修改的是局部变量t, t的值被设置为10。
  5. defer语句执行完后RET返回,此时函数的返回值是5。
    图解如下
f2指令序列 内存值
r=0 r=0
t=5 t=5
copy t to r r=5
defer t=t+5 t=10
RET r=5

f3

f3函数的逻辑如下:

  1. 首先r被初始化为0.
  2. 复制1到返回值r所在的栈区。
  3. 执行defer语句,由于匿名函数使用的传参调用,修改的是匿名函数的形参,对返回值r无影响。
  4. defer语句执行完后RET返回,此时函数的返回值是1。
    图解如下

//r1为返回值, r2为匿名函数形参

f3指令序列 内存值
r1=0 r1=0
copy r1 to r2 r2=0
defer r2=r2+5 r2=5
copy 1 to r1 r1=1
RET r1=1

总结

对于带defer的函数返回整体上有三个步骤。
1. 执行return的值拷贝,将return语句返回的值复制到函数返回值栈区。
2. 执行defer语句,多个defer按照FILO顺序执行。
3. 执行调整RET指令。

其实在defer中修改函数返回值本身是种不良的编程实践,不推荐。

汇编解答

预备知识

  1. return x不是一个原子命令,return x分为了两步,第一步将返回值置为x,最后RET,使PC恢复到调用位置的下一个指令位置;
  2. 返回值,在程序中是如何处理的,更深入点,就是在机器码或者汇编上是如何处理的。要知道在调用函数前,首先要处理参数和返回值,go语言的参数和返回值都是通过栈传递的,调用前,返回值入栈,参数N入栈,…,参数1入栈。所以return x,就是要将x传递到函数调用前的栈中;
  3. defer是在返回前调用的,即在RET前。

简而言之就是

返回值 = xxx
调用defer函数
空的return

栈结构图

+———–+———–
| 返回值N |
+———–+
| … |
+———–+
| 返回值1 |
+———+-+
| 参数2 | 在调用函数中,上述代码就是main函数中
+———–+
| … |
+———–+
| 参数1 |
+———–+
| 返回地址 | 调用结束后PC指向的位置
+———–+———bp值
| 局部变量 |
| … | 被调用数栈祯
| |
+———–+———sp值

f1

"".f1 STEXT size=124 args=0x8 locals=0x20
        0x0000 00000 (main.go:13)       TEXT    "".f1(SB), ABIInternal, $32-8 ;函数栈大小32,参数大小8
        0x0000 00000 (main.go:13)       MOVQ    (TLS), CX
        0x0009 00009 (main.go:13)       CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:13)       JLS     117
        0x000f 00015 (main.go:13)       SUBQ    $32, SP ;开始SP应该在本函数的虚拟地址32位置,运行时应指到32-32=0的位置
        0x0013 00019 (main.go:13)       MOVQ    BP, 24(SP) ;调用f1的函数,这里即main函数的BP,保存在当前函数栈24(SP)下
        0x0018 00024 (main.go:13)       LEAQ    24(SP), BP ;lea不解引用,即直接把24(sp)的地址复制到BP,而不是24(SP)的内容给BP,即BP指向24位置
        0x001d 00029 (main.go:13)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:13)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:13)       FUNCDATA        $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x001d 00029 (main.go:13)       PCDATA  $2, $0
        0x001d 00029 (main.go:13)       PCDATA  $0, $0
        0x001d 00029 (main.go:13)       MOVQ    $0, "".r+40(SP) ;返回值在40位置,而本函数栈最高32,即r在该函数外,即是调用者的栈中
        0x0026 00038 (main.go:14)       MOVL    $8, (SP);8复制到0(SP)上,deferproc有两个参数,这里可能是其参数1,表示defer的函数f1需要8B大小的参数,即那个闭包参数r
        0x002d 00045 (main.go:14)       PCDATA  $2, $1
        0x002d 00045 (main.go:14)       LEAQ    "".f1.func1·f(SB), AX;将匿名函数地址复制到AX中
        0x0034 00052 (main.go:14)       PCDATA  $2, $0
        0x0034 00052 (main.go:14)       MOVQ    AX, 8(SP) ;f1.func1放在栈的8位置上,因为没有参数,所以接下来我们没有看到处理参数部分
        0x0039 00057 (main.go:14)       PCDATA  $2, $1
        0x0039 00057 (main.go:14)       LEAQ    "".r+40(SP), AX ;40(sp)的地址复制到AX中,这里我们就认为是40了,这是虚拟地址
        0x003e 00062 (main.go:14)       PCDATA  $2, $0
        0x003e 00062 (main.go:14)       MOVQ    AX, 16(SP) ;16(SP)记录的是r的地址
        0x0043 00067 (main.go:14)       CALL    runtime.deferproc(SB) ;注册defer函数
        0x0048 00072 (main.go:14)       TESTL   AX, AX
        0x004a 00074 (main.go:14)       JNE     101
        0x004c 00076 (main.go:17)       MOVQ    $0, "".r+40(SP)
        0x0055 00085 (main.go:17)       XCHGL   AX, AX
        0x0056 00086 (main.go:17)       CALL    runtime.deferreturn(SB);真正的调用defer,闭包中的r值会在这改变, 
        0x005b 00091 (main.go:17)       MOVQ    24(SP), BP;24(SP)记录的是f1的调用者main的BP值,所以,这是要返回了
        0x0060 00096 (main.go:17)       ADDQ    $32, SP;SP=SP+32,回到调用f1的位置
        0x0064 00100 (main.go:17)       RET  ;return返回,PC跳到调用函数的下一条指令,defer在RET前,但返回值的赋值在defer之前
        0x0065 00101 (main.go:14)       XCHGL   AX, AX
        0x0066 00102 (main.go:14)       CALL    runtime.deferreturn(SB)
        0x006b 00107 (main.go:14)       MOVQ    24(SP), BP
        0x0070 00112 (main.go:14)       ADDQ    $32, SP
        0x0074 00116 (main.go:14)       RET
        0x0075 00117 (main.go:14)       NOP
        0x0075 00117 (main.go:13)       PCDATA  $0, $-1
        0x0075 00117 (main.go:13)       PCDATA  $2, $-1
        0x0075 00117 (main.go:13)       CALL    runtime.morestack_noctxt(SB)
        0x007a 00122 (main.go:13)       JMP     0
        0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 66 48  eH..%....H;a.vfH
        0x0010 83 ec 20 48 89 6c 24 18 48 8d 6c 24 18 48 c7 44  .. H.l$.H.l$.H.D
        0x0020 24 28 00 00 00 00 c7 04 24 08 00 00 00 48 8d 05  $(......$....H..
        0x0030 00 00 00 00 48 89 44 24 08 48 8d 44 24 28 48 89  ....H.D$.H.D$(H.
        0x0040 44 24 10 e8 00 00 00 00 85 c0 75 19 48 c7 44 24  D$........u.H.D$
        0x0050 28 00 00 00 00 90 e8 00 00 00 00 48 8b 6c 24 18  (..........H.l$.
        0x0060 48 83 c4 20 c3 90 e8 00 00 00 00 48 8b 6c 24 18  H.. .......H.l$.
        0x0070 48 83 c4 20 c3 e8 00 00 00 00 eb 84              H.. ........
        rel 5+4 t=16 TLS+0
        rel 48+4 t=15 "".f1.func1·f+0
        rel 68+4 t=8 runtime.deferproc+0
        rel 87+4 t=8 runtime.deferreturn+0
        rel 103+4 t=8 runtime.deferreturn+0
        rel 118+4 t=8 runtime.morestack_noctxt+0

f2

"".f2 STEXT size=137 args=0x8 locals=0x28
        0x0000 00000 (main.go:20)       TEXT    "".f2(SB), ABIInternal, $40-8
        0x0000 00000 (main.go:20)       MOVQ    (TLS), CX
        0x0009 00009 (main.go:20)       CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:20)       JLS     127
        0x000f 00015 (main.go:20)       SUBQ    $40, SP
        0x0013 00019 (main.go:20)       MOVQ    BP, 32(SP)
        0x0018 00024 (main.go:20)       LEAQ    32(SP), BP
        0x001d 00029 (main.go:20)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:20)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:20)       FUNCDATA        $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x001d 00029 (main.go:20)       PCDATA  $2, $0
        0x001d 00029 (main.go:20)       PCDATA  $0, $0
        0x001d 00029 (main.go:20)       MOVQ    $0, "".r+48(SP)
        0x0026 00038 (main.go:21)       MOVQ    $5, "".t+24(SP)
        0x002f 00047 (main.go:22)       MOVL    $8, (SP)
        0x0036 00054 (main.go:22)       PCDATA  $2, $1
        0x0036 00054 (main.go:22)       LEAQ    "".f2.func1·f(SB), AX
        0x003d 00061 (main.go:22)       PCDATA  $2, $0
        0x003d 00061 (main.go:22)       MOVQ    AX, 8(SP)
        0x0042 00066 (main.go:22)       PCDATA  $2, $1
        0x0042 00066 (main.go:22)       LEAQ    "".t+24(SP), AX
        0x0047 00071 (main.go:22)       PCDATA  $2, $0
        0x0047 00071 (main.go:22)       MOVQ    AX, 16(SP)
        0x004c 00076 (main.go:22)       CALL    runtime.deferproc(SB)
        0x0051 00081 (main.go:22)       TESTL   AX, AX
        0x0053 00083 (main.go:22)       JNE     111
        0x0055 00085 (main.go:25)       MOVQ    "".t+24(SP), AX
        0x005a 00090 (main.go:25)       MOVQ    AX, "".r+48(SP)
        0x005f 00095 (main.go:25)       XCHGL   AX, AX
        0x0060 00096 (main.go:25)       CALL    runtime.deferreturn(SB)
        0x0065 00101 (main.go:25)       MOVQ    32(SP), BP
        0x006a 00106 (main.go:25)       ADDQ    $40, SP
        0x006e 00110 (main.go:25)       RET
        0x006f 00111 (main.go:22)       XCHGL   AX, AX
        0x0070 00112 (main.go:22)       CALL    runtime.deferreturn(SB)
        0x0075 00117 (main.go:22)       MOVQ    32(SP), BP
        0x007a 00122 (main.go:22)       ADDQ    $40, SP
        0x007e 00126 (main.go:22)       RET
        0x007f 00127 (main.go:22)       NOP
        0x007f 00127 (main.go:20)       PCDATA  $0, $-1
        0x007f 00127 (main.go:20)       PCDATA  $2, $-1
        0x007f 00127 (main.go:20)       CALL    runtime.morestack_noctxt(SB)
        0x0084 00132 (main.go:20)       JMP     0
        0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 70 48  eH..%....H;a.vpH
        0x0010 83 ec 28 48 89 6c 24 20 48 8d 6c 24 20 48 c7 44  ..(H.l$ H.l$ H.D
        0x0020 24 30 00 00 00 00 48 c7 44 24 18 05 00 00 00 c7  $0....H.D$......
        0x0030 04 24 08 00 00 00 48 8d 05 00 00 00 00 48 89 44  .$....H......H.D
        0x0040 24 08 48 8d 44 24 18 48 89 44 24 10 e8 00 00 00  $.H.D$.H.D$.....
        0x0050 00 85 c0 75 1a 48 8b 44 24 18 48 89 44 24 30 90  ...u.H.D$.H.D$0.
        0x0060 e8 00 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 90  .....H.l$ H..(..
        0x0070 e8 00 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 e8  .....H.l$ H..(..
        0x0080 00 00 00 00 e9 77 ff ff ff                       .....w...
        rel 5+4 t=16 TLS+0
        rel 57+4 t=15 "".f2.func1·f+0
        rel 77+4 t=8 runtime.deferproc+0
        rel 97+4 t=8 runtime.deferreturn+0
        rel 113+4 t=8 runtime.deferreturn+0
        rel 128+4 t=8 runtime.morestack_noctxt+0


f3

"".f3 STEXT size=123 args=0x8 locals=0x20
        0x0000 00000 (main.go:28)       TEXT    "".f3(SB), ABIInternal, $32-8
        0x0000 00000 (main.go:28)       MOVQ    (TLS), CX
        0x0009 00009 (main.go:28)       CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:28)       JLS     116
        0x000f 00015 (main.go:28)       SUBQ    $32, SP
        0x0013 00019 (main.go:28)       MOVQ    BP, 24(SP)
        0x0018 00024 (main.go:28)       LEAQ    24(SP), BP
        0x001d 00029 (main.go:28)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:28)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x001d 00029 (main.go:28)       FUNCDATA        $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x001d 00029 (main.go:28)       PCDATA  $2, $0
        0x001d 00029 (main.go:28)       PCDATA  $0, $0
        0x001d 00029 (main.go:28)       MOVQ    $0, "".r+40(SP)
        0x0026 00038 (main.go:29)       MOVL    $8, (SP)
        0x002d 00045 (main.go:29)       PCDATA  $2, $1
        0x002d 00045 (main.go:29)       LEAQ    "".f3.func1·f(SB), AX
        0x0034 00052 (main.go:29)       PCDATA  $2, $0
        0x0034 00052 (main.go:29)       MOVQ    AX, 8(SP)
        0x0039 00057 (main.go:29)       MOVQ    $0, 16(SP)
        0x0042 00066 (main.go:29)       CALL    runtime.deferproc(SB)
        0x0047 00071 (main.go:29)       TESTL   AX, AX
        0x0049 00073 (main.go:29)       JNE     100
        0x004b 00075 (main.go:32)       MOVQ    $1, "".r+40(SP)
        0x0054 00084 (main.go:32)       XCHGL   AX, AX
        0x0055 00085 (main.go:32)       CALL    runtime.deferreturn(SB)
        0x005a 00090 (main.go:32)       MOVQ    24(SP), BP
        0x005f 00095 (main.go:32)       ADDQ    $32, SP
        0x0063 00099 (main.go:32)       RET
        0x0064 00100 (main.go:29)       XCHGL   AX, AX
        0x0065 00101 (main.go:29)       CALL    runtime.deferreturn(SB)
        0x006a 00106 (main.go:29)       MOVQ    24(SP), BP
        0x006f 00111 (main.go:29)       ADDQ    $32, SP
        0x0073 00115 (main.go:29)       RET
        0x0074 00116 (main.go:29)       NOP
        0x0074 00116 (main.go:28)       PCDATA  $0, $-1
        0x0074 00116 (main.go:28)       PCDATA  $2, $-1
        0x0074 00116 (main.go:28)       CALL    runtime.morestack_noctxt(SB)
        0x0079 00121 (main.go:28)       JMP     0
        0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 65 48  eH..%....H;a.veH
        0x0010 83 ec 20 48 89 6c 24 18 48 8d 6c 24 18 48 c7 44  .. H.l$.H.l$.H.D
        0x0020 24 28 00 00 00 00 c7 04 24 08 00 00 00 48 8d 05  $(......$....H..
        0x0030 00 00 00 00 48 89 44 24 08 48 c7 44 24 10 00 00  ....H.D$.H.D$...
        0x0040 00 00 e8 00 00 00 00 85 c0 75 19 48 c7 44 24 28  .........u.H.D$(
        0x0050 01 00 00 00 90 e8 00 00 00 00 48 8b 6c 24 18 48  ..........H.l$.H
        0x0060 83 c4 20 c3 90 e8 00 00 00 00 48 8b 6c 24 18 48  .. .......H.l$.H
        0x0070 83 c4 20 c3 e8 00 00 00 00 eb 85                 .. ........
        rel 5+4 t=16 TLS+0
        rel 48+4 t=15 "".f3.func1·f+0
        rel 67+4 t=8 runtime.deferproc+0
        rel 86+4 t=8 runtime.deferreturn+0
        rel 102+4 t=8 runtime.deferreturn+0
        rel 117+4 t=8 runtime.morestack_noctxt+0

参考

  1. 主要摘抄自《go语言核心编程》
  2. 用汇编分析go的defer闭包处理