Go语言学习——彻底弄懂return和defer的微妙关系

疑问

前面在函数篇里介绍了Go语言的函数是支持多返回值的。

只要在函数体内,对返回值赋值,最后加上return就可以返回所有的返回值。

最近在写代码的时候经常遇到在return后,还要在defer里面做一些收尾工作,比如事务的提交或回滚。所以想弄清楚这个return和defer到底是什么关系,它们谁先谁后,对于最后返回值又有什么影响呢?

动手验证

了解下来,问题比我想的要复杂,不信你先看看下面这段代码输出结果是啥

1package main 2 3import "fmt" 4 5func main() { 6 fmt.Println("f1 result: ", f1()) 7 fmt.Println("f2 result: ", f2()) 8} 9 10func f1() int { 11 var i int 12 defer func() { 13 i++ 14 fmt.Println("f11: ", i) 15 }() 16 17 defer func() { 18 i++ 19 fmt.Println("f12: ", i) 20 }() 21 22 i = 1000 23 return i 24} 25 26func f2() (i int) { 27 defer func() { 28 i++ 29 fmt.Println("f21: ", i) 30 }() 31 32 defer func() { 33 i++ 34 fmt.Println("f22: ", i) 35 }() 36 37 i = 1000 38 return i 39}

最后的执行结果如下

1f12: 1001 2f11: 1002 3f1 result: 1000 4f22: 1001 5f21: 1002 6f2 result: 1002

f1函数:

进入该函数,因为没有指定返回值变量,需要先声明i变量,因为是int类型,如果没有赋值,该变量初始化值为0,之后执行i=1000的赋值操作,然后执行return语句,返回i的值。

真正返回之前还要执行defer函数部分,两个defer函数分别针对i进行自增操作,i的值依次为1001和1002

f2函数:

进入该函数,因为已经定义好了返回值变量即为i,然后直接赋值i=1000,再返回i的值。

同样的,也要在真正返回i前,执行两个defer函数,同样i依次自增得到1001和1002。

问题的关键是为什么无名参数返回的值是1000,其并未收到defer函数对于i自增的影响;而有名函数在执行defer后,最后返回的i值为1002。

网上找了一些原因,提到一个结论

1原因就是return会将返回值先保存起来,对于无名返回值来说, 2保存在一个临时对象中,defer是看不到这个临时对象的; 3而对于有名返回值来说,就保存在已命名的变量中。

看到这个结论,我想试试通过打印i的地址值是否可以看出一些端倪和线索

为此在两个函数中添加了打印i的地址信息

1package main 2 3import "fmt" 4 5func main() { 6 fmt.Println("f1 result: ", f1()) 7 fmt.Println("f2 result: ", f2()) 8} 9 10func f1() int { 11 var i int 12 fmt.Printf("i: %p \n", &i) 13 defer func() { 14 i++ 15 fmt.Printf("i: %p \n", &i) 16 fmt.Println("f11: ", i) 17 }() 18 19 defer func() { 20 i++ 21 fmt.Printf("i: %p \n", &i) 22 fmt.Println("f12: ", i) 23 }() 24 25 i = 1000 26 return i 27} 28 29func f2() (i int) { 30 fmt.Printf("i: %p \n", &i) 31 defer func() { 32 i++ 33 fmt.Printf("i: %p \n", &i) 34 fmt.Println("f21: ", i) 35 }() 36 37 defer func() { 38 i++ 39 fmt.Printf("i: %p \n", &i) 40 fmt.Println("f22: ", i) 41 }() 42 i = 1000 43 return i 44}

程序输出结果为

1i: 0xc000090000 2i: 0xc000090000 3f12: 1001 4i: 0xc000090000 5f11: 1002 6f1 result: 1000 7i: 0xc00009a008 8i: 0xc00009a008 9f22: 1001 10i: 0xc00009a008 11f21: 1002 12f2 result: 1002

从这个结果可以看出,无论是f1还是f2函数中,变量i的地址全程没有改变过。

所以对于上面这个结论我似乎懂了,但是还是有些模糊,return保存在一个临时对象中,defer看不到这个临时变量,但是i的值为什么能够在1000的基础上累加呢?

拨开云雾

如果要从根本解决这个疑问,最好能够看看这段程序执行,背后的内存是如何分配的。

这时候想到了前几天看书里提到的可以通过命令将go语言转为汇编语言。

为了简化问题,将源代码修改为

1package main 2 3import "fmt" 4 5func main() { 6 fmt.Println("f1 result: ", f1()) 7 fmt.Println("f2 result: ", f2()) 8} 9 10func f1() int { 11 var i int 12 defer func() { 13 i++ 14 fmt.Println("f11: ", i) 15 }() 16 17 i = 1000 18 return i 19} 20 21func f2() (i int) { 22 defer func() { 23 i++ 24 fmt.Println("f21: ", i) 25 }() 26 i = 1000 27 return i 28}

通过执行命令go tool compile -S test.go得到汇编代码如下

1os.(*File).close STEXT dupok nosplit size=26 args=0x18 locals=0x0 2 ... 3 0x0000 00000 (test.go:5) TEXT "".main(SB), ABIInternal, $136-0 4 0x0000 00000 (test.go:5) MOVQ (TLS), CX 5 0x0009 00009 (test.go:5) LEAQ -8(SP), AX 6 0x000e 00014 (test.go:5) CMPQ AX, 16(CX) 7 0x0012 00018 (test.go:5) JLS 315 8 0x0018 00024 (test.go:5) SUBQ $136, SP 9 0x001f 00031 (test.go:5) MOVQ BP, 128(SP) 10 0x0027 00039 (test.go:5) LEAQ 128(SP), BP 11 0x002f 00047 (test.go:5) FUNCDATA $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB) 12 ... 13"".f1 STEXT size=145 args=0x8 locals=0x28 14 0x0000 00000 (test.go:10) TEXT "".f1(SB), ABIInternal, $40-8 15 0x0000 00000 (test.go:10) MOVQ (TLS), CX 16 0x0009 00009 (test.go:10) CMPQ SP, 16(CX) 17 0x000d 00013 (test.go:10) JLS 135 18 0x000f 00015 (test.go:10) SUBQ $40, SP 19 0x0013 00019 (test.go:10) MOVQ BP, 32(SP) 20 0x0018 00024 (test.go:10) LEAQ 32(SP), BP 21 0x001d 00029 (test.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 22 0x001d 00029 (test.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 23 0x001d 00029 (test.go:10) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 24 0x001d 00029 (test.go:10) PCDATA $2, $0 25 0x001d 00029 (test.go:10) PCDATA $0, $0 26 0x001d 00029 (test.go:10) MOVQ $0, "".~r0+48(SP) 27 0x0026 00038 (test.go:11) MOVQ $0, "".i+24(SP) 28 0x002f 00047 (test.go:12) MOVL $8, (SP) 29 0x0036 00054 (test.go:12) PCDATA $2, $1 30 0x0036 00054 (test.go:12) LEAQ "".f1.func1·f(SB), AX 31 0x003d 00061 (test.go:12) PCDATA $2, $0 32 0x003d 00061 (test.go:12) MOVQ AX, 8(SP) 33 0x0042 00066 (test.go:12) PCDATA $2, $1 34 0x0042 00066 (test.go:12) LEAQ "".i+24(SP), AX 35 0x0047 00071 (test.go:12) PCDATA $2, $0 36 0x0047 00071 (test.go:12) MOVQ AX, 16(SP) 37 0x004c 00076 (test.go:12) CALL runtime.deferproc(SB) 38 0x0051 00081 (test.go:12) TESTL AX, AX 39 0x0053 00083 (test.go:12) JNE 119 40 0x0055 00085 (test.go:17) MOVQ $1000, "".i+24(SP) 41 0x005e 00094 (test.go:18) MOVQ $1000, "".~r0+48(SP) 42 0x0067 00103 (test.go:18) XCHGL AX, AX 43 0x0068 00104 (test.go:18) CALL runtime.deferreturn(SB) 44 0x006d 00109 (test.go:18) MOVQ 32(SP), BP 45 0x0072 00114 (test.go:18) ADDQ $40, SP 46 0x0076 00118 (test.go:18) RET 47 0x0077 00119 (test.go:12) XCHGL AX, AX 48 0x0078 00120 (test.go:12) CALL runtime.deferreturn(SB) 49 0x007d 00125 (test.go:12) MOVQ 32(SP), BP 50 0x0082 00130 (test.go:12) ADDQ $40, SP 51 0x0086 00134 (test.go:12) RET 52 0x0087 00135 (test.go:12) NOP 53 0x0087 00135 (test.go:10) PCDATA $0, $-1 54 0x0087 00135 (test.go:10) PCDATA $2, $-1 55 0x0087 00135 (test.go:10) CALL runtime.morestack_noctxt(SB) 56 0x008c 00140 (test.go:10) JMP 0 57 ... 58 0x0000 00000 (test.go:21) TEXT "".f2(SB), ABIInternal, $32-8 59 0x0000 00000 (test.go:21) MOVQ (TLS), CX 60 0x0009 00009 (test.go:21) CMPQ SP, 16(CX) 61 0x000d 00013 (test.go:21) JLS 117 62 0x000f 00015 (test.go:21) SUBQ $32, SP 63 0x0013 00019 (test.go:21) MOVQ BP, 24(SP) 64 0x0018 00024 (test.go:21) LEAQ 24(SP), BP 65 0x001d 00029 (test.go:21) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 66 0x001d 00029 (test.go:21) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 67 0x001d 00029 (test.go:21) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 68 0x001d 00029 (test.go:21) PCDATA $2, $0 69 0x001d 00029 (test.go:21) PCDATA $0, $0 70 0x001d 00029 (test.go:21) MOVQ $0, "".i+40(SP) 71 0x0026 00038 (test.go:22) MOVL $8, (SP) 72 0x002d 00045 (test.go:22) PCDATA $2, $1 73 0x002d 00045 (test.go:22) LEAQ "".f2.func1·f(SB), AX 74 0x0034 00052 (test.go:22) PCDATA $2, $0 75 0x0034 00052 (test.go:22) MOVQ AX, 8(SP) 76 0x0039 00057 (test.go:22) PCDATA $2, $1 77 0x0039 00057 (test.go:22) LEAQ "".i+40(SP), AX 78 0x003e 00062 (test.go:22) PCDATA $2, $0 79 0x003e 00062 (test.go:22) MOVQ AX, 16(SP) 80 0x0043 00067 (test.go:22) CALL runtime.deferproc(SB) 81 0x0048 00072 (test.go:22) TESTL AX, AX 82 0x004a 00074 (test.go:22) JNE 101 83 0x004c 00076 (test.go:26) MOVQ $1000, "".i+40(SP) 84 0x0055 00085 (test.go:27) XCHGL AX, AX 85 0x0056 00086 (test.go:27) CALL runtime.deferreturn(SB) 86 0x005b 00091 (test.go:27) MOVQ 24(SP), BP 87 0x0060 00096 (test.go:27) ADDQ $32, SP 88 0x0064 00100 (test.go:27) RET 89 0x0065 00101 (test.go:22) XCHGL AX, AX 90 0x0066 00102 (test.go:22) CALL runtime.deferreturn(SB) 91 0x006b 00107 (test.go:22) MOVQ 24(SP), BP 92 0x0070 00112 (test.go:22) ADDQ $32, SP 93 0x0074 00116 (test.go:22) RET 94 0x0075 00117 (test.go:22) NOP 95 0x0075 00117 (test.go:21) PCDATA $0, $-1 96 0x0075 00117 (test.go:21) PCDATA $2, $-1 97 0x0075 00117 (test.go:21) CALL runtime.morestack_noctxt(SB) 98 0x007a 00122 (test.go:21) JMP 0 99 ... ........ 100 rel 16+8 t=1 type.[2]interface {}+0

感觉离真相只差一步了,就是看完这段汇编代码就能搞明白这个return在无名和有名返回值时分别做了什么,所谓的零时变量是咋分配的,想想就有点小激动呢

但是,比较棘手的是,我没学过汇编-_-!

但是again,这有什么关系呢,两个函数既然执行结果不一样,那么在汇编层面肯定也有不一样的地方,于是开始找不同,最终在上面的汇编代码分别找到关键信息如下

1"".f2 STEXT size=124 args=0x8 locals=0x20 2 0x0000 00000 (test.go:21) TEXT "".f2(SB), ABIInternal, $32-8 3 0x0000 00000 (test.go:21) MOVQ (TLS), CX 4 0x0009 00009 (test.go:21) CMPQ SP, 16(CX) 5 0x000d 00013 (test.go:21) JLS 117 6 0x000f 00015 (test.go:21) SUBQ $32, SP 7 0x0013 00019 (test.go:21) MOVQ BP, 24(SP) 8 0x0018 00024 (test.go:21) LEAQ 24(SP), BP 9 0x001d 00029 (test.go:21) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 10 0x001d 00029 (test.go:21) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 11 0x001d 00029 (test.go:21) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 12 0x001d 00029 (test.go:21) PCDATA $2, $0 13 0x001d 00029 (test.go:21) PCDATA $0, $0 14 0x001d 00029 (test.go:21) MOVQ $0, "".i+40(SP) 15 0x0026 00038 (test.go:22) MOVL $8, (SP) 16 0x002d 00045 (test.go:22) PCDATA $2, $1 17 0x002d 00045 (test.go:22) LEAQ "".f2.func1·f(SB), AX 18 0x0034 00052 (test.go:22) PCDATA $2, $0 19 0x0034 00052 (test.go:22) MOVQ AX, 8(SP) 20 0x0039 00057 (test.go:22) PCDATA $2, $1 21 0x0039 00057 (test.go:22) LEAQ "".i+40(SP), AX 22 0x003e 00062 (test.go:22) PCDATA $2, $0 23 0x003e 00062 (test.go:22) MOVQ AX, 16(SP) 24 0x0043 00067 (test.go:22) CALL runtime.deferproc(SB) 25 0x0048 00072 (test.go:22) TESTL AX, AX 26 0x004a 00074 (test.go:22) JNE 101 27 0x004c 00076 (test.go:26) MOVQ $1000, "".i+40(SP) 28 0x0055 00085 (test.go:27) XCHGL AX, AX 29 0x0056 00086 (test.go:27) CALL runtime.deferreturn(SB) 30 0x005b 00091 (test.go:27) MOVQ 24(SP), BP 31 0x0060 00096 (test.go:27) ADDQ $32, SP 32 0x0064 00100 (test.go:27) RET 33 0x0065 00101 (test.go:22) XCHGL AX, AX 34 0x0066 00102 (test.go:22) CALL runtime.deferreturn(SB) 35 0x006b 00107 (test.go:22) MOVQ 24(SP), BP 36 0x0070 00112 (test.go:22) ADDQ $32, SP 37 0x0074 00116 (test.go:22) RET 38 0x0075 00117 (test.go:22) NOP 39 0x0075 00117 (test.go:21) PCDATA $0, $-1 40 0x0075 00117 (test.go:21) PCDATA $2, $-1 41 0x0075 00117 (test.go:21) CALL runtime.morestack_noctxt(SB) 42 0x007a 00122 (test.go:21) JMP 0

这是f2有名返回值的关键信息,主要看

 0x004c 00076 (test.go:26)   MOVQ    $1000, "".i+40(SP) 

这个大概意思就是把1000放到"".i+40(SP)这个内存地址上,然后下面执行的操作就是返回了

1"".f1 STEXT size=145 args=0x8 locals=0x28 2 0x0000 00000 (test.go:10) TEXT "".f1(SB), ABIInternal, $40-8 3 0x0000 00000 (test.go:10) MOVQ (TLS), CX 4 0x0009 00009 (test.go:10) CMPQ SP, 16(CX) 5 0x000d 00013 (test.go:10) JLS 135 6 0x000f 00015 (test.go:10) SUBQ $40, SP 7 0x0013 00019 (test.go:10) MOVQ BP, 32(SP) 8 0x0018 00024 (test.go:10) LEAQ 32(SP), BP 9 0x001d 00029 (test.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 10 0x001d 00029 (test.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 11 0x001d 00029 (test.go:10) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 12 0x001d 00029 (test.go:10) PCDATA $2, $0 13 0x001d 00029 (test.go:10) PCDATA $0, $0 14 0x001d 00029 (test.go:10) MOVQ $0, "".~r0+48(SP) 15 0x0026 00038 (test.go:11) MOVQ $0, "".i+24(SP) 16 0x002f 00047 (test.go:12) MOVL $8, (SP) 17 0x0036 00054 (test.go:12) PCDATA $2, $1 18 0x0036 00054 (test.go:12) LEAQ "".f1.func1·f(SB), AX 19 0x003d 00061 (test.go:12) PCDATA $2, $0 20 0x003d 00061 (test.go:12) MOVQ AX, 8(SP) 21 0x0042 00066 (test.go:12) PCDATA $2, $1 22 0x0042 00066 (test.go:12) LEAQ "".i+24(SP), AX 23 0x0047 00071 (test.go:12) PCDATA $2, $0 24 0x0047 00071 (test.go:12) MOVQ AX, 16(SP) 25 0x004c 00076 (test.go:12) CALL runtime.deferproc(SB) 26 0x0051 00081 (test.go:12) TESTL AX, AX 27 0x0053 00083 (test.go:12) JNE 119 28 0x0055 00085 (test.go:17) MOVQ $1000, "".i+24(SP) 29 0x005e 00094 (test.go:18) MOVQ $1000, "".~r0+48(SP) 30 0x0067 00103 (test.go:18) XCHGL AX, AX 31 0x0068 00104 (test.go:18) CALL runtime.deferreturn(SB) 32 0x006d 00109 (test.go:18) MOVQ 32(SP), BP 33 0x0072 00114 (test.go:18) ADDQ $40, SP 34 0x0076 00118 (test.go:18) RET 35 0x0077 00119 (test.go:12) XCHGL AX, AX 36 0x0078 00120 (test.go:12) CALL runtime.deferreturn(SB) 37 0x007d 00125 (test.go:12) MOVQ 32(SP), BP 38 0x0082 00130 (test.go:12) ADDQ $40, SP 39 0x0086 00134 (test.go:12) RET 40 0x0087 00135 (test.go:12) NOP 41 0x0087 00135 (test.go:10) PCDATA $0, $-1 42 0x0087 00135 (test.go:10) PCDATA $2, $-1 43 0x0087 00135 (test.go:10) CALL runtime.morestack_noctxt(SB) 44 0x008c 00140 (test.go:10) JMP 0

这是f1无名返回值的关键信息,主要看

1 0x0055 00085 (test.go:17) MOVQ $1000, "".i+24(SP) 2 0x005e 00094 (test.go:18) MOVQ $1000, "".~r0+48(SP)

这个大概意思就是把1000放到"".i+24(SP)这个内存地址上,然后又把1000赋给了"".r0+48(SP),这就是和f1不一样的地方。对应前面结论,我们在这里找到了验证。大致过程就是无名返回值的情况,在return的时候开辟了一个新内存空间,后续的defer读取的还是"".i+24(SP)这样的内存地址而无法读取临时空间的值。return在函数最后返回的也是"".r0+48(SP)对应的值即1000。(因为没有研究过汇编,有些细节可能有待考证)

结论

到此,我们算是搞明白了Go语言里面return和defer之间的微妙关系,从汇编层面看清了在无名返回值和有名返回值return返回的差异。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Golang中defer、return、返回值之间执行顺序的坑

原文链接:https://studygolang.com/articles/4809Go语言中延迟函数defer充当着cry...catch的重任,使用起来也非常简便,然而在实际应用中,很多gopher并没有真正搞明白defer、return和返回值之间的执行顺序,从而掉进坑中,今天我们就来揭开它的神秘面纱!先来运行下面两段代码:A.无名返