1. 程式人生 > >Go語言學習——徹底弄懂return和defer的微妙關係

Go語言學習——徹底弄懂return和defer的微妙關係

疑問

  前面在函式篇裡介紹了Go語言的函式是支援多返回值的。

  只要在函式體內,對返回值賦值,最後加上return就可以返回所有的返回值。

  最近在寫程式碼的時候經常遇到在return後,還要在defer裡面做一些收尾工作,比如事務的提交或回滾。所以想弄清楚這個return和defer到底是什麼關係,它們誰先誰後,對於最後返回值又有什麼影響呢?

 

動手驗證

  瞭解下來,問題比我想的要複雜,不信你先看看下面這段程式碼輸出結果是啥

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	defer func() {
		i++
		fmt.Println("f11: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f12: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	defer func() {
		i++
		fmt.Println("f21: ", i)
	}()

	defer func() {
		i++
		fmt.Println("f22: ", i)
	}()

	i = 1000
	return i
}

  

  最後的執行結果如下

f12: 1001
f11: 1002
f1 result: 1000
f22: 1001
f21: 1002
f2 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。

  網上找了一些原因,提到一個結論


 原因就是return會將返回值先儲存起來,對於無名返回值來說,
 儲存在一個臨時物件中,defer是看不到這個臨時物件的;
 而對於有名返回值來說,就儲存在已命名的變數中。

  看到這個結論,我想試試通過列印i的地址值是否可以看出一些端倪和線索

 

  為此在兩個函式中添加了列印i的地址資訊

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	fmt.Printf("i: %p \n", &i)
	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f11: ", i)
	}()

	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f12: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	fmt.Printf("i: %p \n", &i)
	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f21: ", i)
	}()

	defer func() {
		i++
		fmt.Printf("i: %p \n", &i)
		fmt.Println("f22: ", i)
	}()
    i = 1000
	return i
}

  

 

  程式輸出結果為

i: 0xc000090000 
i: 0xc000090000 
f12:  1001
i: 0xc000090000 
f11:  1002
f1 result:  1000
i: 0xc00009a008 
i: 0xc00009a008 
f22:  1001
i: 0xc00009a008 
f21:  1002
f2 result:  1002

  

  從這個結果可以看出,無論是f1還是f2函式中,變數i的地址全程沒有改變過。

  所以對於上面這個結論我似乎懂了,但是還是有些模糊,return儲存在一個臨時物件中,defer看不到這個臨時變數,但是i的值為什麼能夠在1000的基礎上累加呢?

 

撥開雲霧

  如果要從根本解決這個疑問,最好能夠看看這段程式執行,背後的記憶體是如何分配的。

  這時候想到了前幾天看書裡提到的可以通過命令將go語言轉為組合語言。

  為了簡化問題,將原始碼修改為

package main

import "fmt"

func main()  {
	fmt.Println("f1 result: ", f1())
	fmt.Println("f2 result: ", f2())
}

func f1() int {
	var i int
	defer func() {
		i++
		fmt.Println("f11: ", i)
	}()

	i = 1000
	return i
}

func f2() (i int) {
	defer func() {
		i++
		fmt.Println("f21: ", i)
	}()
	i = 1000
	return i
}

  

  通過執行命令go tool compile -S test.go得到彙編程式碼如下

os.(*File).close STEXT dupok nosplit size=26 args=0x18 locals=0x0
	...
	0x0000 00000 (test.go:5)	TEXT	"".main(SB), ABIInternal, $136-0
	0x0000 00000 (test.go:5)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:5)	LEAQ	-8(SP), AX
	0x000e 00014 (test.go:5)	CMPQ	AX, 16(CX)
	0x0012 00018 (test.go:5)	JLS	315
	0x0018 00024 (test.go:5)	SUBQ	$136, SP
	0x001f 00031 (test.go:5)	MOVQ	BP, 128(SP)
	0x0027 00039 (test.go:5)	LEAQ	128(SP), BP
	0x002f 00047 (test.go:5)	FUNCDATA	$0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
	...
"".f1 STEXT size=145 args=0x8 locals=0x28
	0x0000 00000 (test.go:10)	TEXT	"".f1(SB), ABIInternal, $40-8
	0x0000 00000 (test.go:10)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:10)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:10)	JLS	135
	0x000f 00015 (test.go:10)	SUBQ	$40, SP
	0x0013 00019 (test.go:10)	MOVQ	BP, 32(SP)
	0x0018 00024 (test.go:10)	LEAQ	32(SP), BP
	0x001d 00029 (test.go:10)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:10)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:10)	PCDATA	$2, $0
	0x001d 00029 (test.go:10)	PCDATA	$0, $0
	0x001d 00029 (test.go:10)	MOVQ	$0, "".~r0+48(SP)
	0x0026 00038 (test.go:11)	MOVQ	$0, "".i+24(SP)
	0x002f 00047 (test.go:12)	MOVL	$8, (SP)
	0x0036 00054 (test.go:12)	PCDATA	$2, $1
	0x0036 00054 (test.go:12)	LEAQ	"".f1.func1·f(SB), AX
	0x003d 00061 (test.go:12)	PCDATA	$2, $0
	0x003d 00061 (test.go:12)	MOVQ	AX, 8(SP)
	0x0042 00066 (test.go:12)	PCDATA	$2, $1
	0x0042 00066 (test.go:12)	LEAQ	"".i+24(SP), AX
	0x0047 00071 (test.go:12)	PCDATA	$2, $0
	0x0047 00071 (test.go:12)	MOVQ	AX, 16(SP)
	0x004c 00076 (test.go:12)	CALL	runtime.deferproc(SB)
	0x0051 00081 (test.go:12)	TESTL	AX, AX
	0x0053 00083 (test.go:12)	JNE	119
	0x0055 00085 (test.go:17)	MOVQ	$1000, "".i+24(SP)
	0x005e 00094 (test.go:18)	MOVQ	$1000, "".~r0+48(SP)
	0x0067 00103 (test.go:18)	XCHGL	AX, AX
	0x0068 00104 (test.go:18)	CALL	runtime.deferreturn(SB)
	0x006d 00109 (test.go:18)	MOVQ	32(SP), BP
	0x0072 00114 (test.go:18)	ADDQ	$40, SP
	0x0076 00118 (test.go:18)	RET
	0x0077 00119 (test.go:12)	XCHGL	AX, AX
	0x0078 00120 (test.go:12)	CALL	runtime.deferreturn(SB)
	0x007d 00125 (test.go:12)	MOVQ	32(SP), BP
	0x0082 00130 (test.go:12)	ADDQ	$40, SP
	0x0086 00134 (test.go:12)	RET
	0x0087 00135 (test.go:12)	NOP
	0x0087 00135 (test.go:10)	PCDATA	$0, $-1
	0x0087 00135 (test.go:10)	PCDATA	$2, $-1
	0x0087 00135 (test.go:10)	CALL	runtime.morestack_noctxt(SB)
	0x008c 00140 (test.go:10)	JMP	0
	...
	0x0000 00000 (test.go:21)	TEXT	"".f2(SB), ABIInternal, $32-8
	0x0000 00000 (test.go:21)	MOVQ	(TLS), CX
	0x0009 00009 (test.go:21)	CMPQ	SP, 16(CX)
	0x000d 00013 (test.go:21)	JLS	117
	0x000f 00015 (test.go:21)	SUBQ	$32, SP
	0x0013 00019 (test.go:21)	MOVQ	BP, 24(SP)
	0x0018 00024 (test.go:21)	LEAQ	24(SP), BP
	0x001d 00029 (test.go:21)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (test.go:21)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x001d 00029 (test.go:21)	PCDATA	$2, $0
	0x001d 00029 (test.go:21)	PCDATA	$0, $0
	0x001d 00029 (test.go:21)	MOVQ	$0, "".i+40(SP)
	0x0026 00038 (test.go:22)	MOVL	$8, (SP)
	0x002d 00045 (test.go:22)	PCDATA	$2, $1
	0x002d 00045 (test.go:22)	LEAQ	"".f2.func1·f(SB), AX
	0x0034 00052 (test.go:22)	PCDATA	$2, $0
	0x0034 00052 (test.go:22)	MOVQ	AX, 8(SP)
	0x0039 00057 (test.go:22)	PCDATA	$2, $1
	0x0039 00057 (test.go:22)	LEAQ	"".i+40(SP), AX
	0x003e 00062 (test.go:22)	PCDATA	$2, $0
	0x003e 00062 (test.go:22)	MOVQ	AX, 16(SP)
	0x0043 00067 (test.go:22)	CALL	runtime.deferproc(SB)
	0x0048 00072 (test.go:22)	TESTL	AX, AX
	0x004a 00074 (test.go:22)	JNE	101
	0x004c 00076 (test.go:26)	MOVQ	$1000, "".i+40(SP)
	0x0055 00085 (test.go:27)	XCHGL	AX, AX
	0x0056 00086 (test.go:27)	CALL	runtime.deferreturn(SB)
	0x005b 00091 (test.go:27)	MOVQ	24(SP), BP
	0x0060 00096 (test.go:27)	ADDQ	$32, SP
	0x0064 00100 (test.go:27)	RET
	0x0065 00101 (test.go:22)	XCHGL	AX, AX
	0x0066 00102 (test.go:22)	CALL	runtime.deferreturn(SB)
	0x006b 00107 (test.go:22)	MOVQ	24(SP), BP
	0x0070 00112 (test.go:22)	ADDQ	$32, SP
	0x0074 00116 (test.go:22)	RET
	0x0075 00117 (test.go:22)	NOP
	0x0075 00117 (test.go:21)	PCDATA	$0, $-1
	0x0075 00117 (test.go:21)	PCDATA	$2, $-1
	0x0075 00117 (test.go:21)	CALL	runtime.morestack_noctxt(SB)
	0x007a 00122 (test.go:21)	JMP	0
	...                    ........
	rel 16+8 t=1 type.[2]interface {}+0

  

  感覺離真相只差一步了,就是看完這段彙編程式碼就能搞明白這個return在無名和有名返回值時分別做了什麼,所謂的零時變數是咋分配的,想想就有點小激動呢

  但是,比較棘手的是,我沒學過彙編-_-!

  但是again,這有什麼關係呢,兩個函式既然執行結果不一樣,那麼在彙編層面肯定也有不一樣的地方,於是開始找不同,最終在上面的彙編程式碼分別找到關鍵資訊如下

 

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

  

  這是f2有名返回值的關鍵資訊,主要看

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

  這個大概意思就是把1000放到"".i+40(SP)這個記憶體地址上,然後下面執行的操作就是返回了

 

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

  

這是f1無名返回值的關鍵資訊,主要看

0x0055 00085 (test.go:17)	MOVQ	$1000, "".i+24(SP)
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返回的差異。

 

如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。