在golang中,對于defer,我之前的了解就是和java中的finally代碼塊一樣,沒什麼難度,但是吧,當我最近看的一些神奇的問題,我就發現原來并非想的那麼簡單。
先舉個栗子
package main
import "fmt"
func main() {
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}
複制
請問這段代碼輸出的結果是什麼?
答案見文末
如果你看完答對了,那麼請直接點選右上角的關閉按鈕,如果你答錯了,你可以繼續往下看了。
下面會一步步介紹,到底為什麼結果會是這樣
基礎知識
函數的傳回值初始化
如 :
func DeferFunc1(i int) (t int) {
其中傳回值t int,這個t會在函數起始處被初始化為對應類型的零值并且作用域為整個函數。
defer的執行順序
雖然這邊沒有提及,但是還是要說一下,因為很多人學習defer的時候都會用到,就是當多個defer出現的時候,它是一個“棧”的關系,也就是先進後出。一個函數中,寫在前面的defer會比寫在後面的defer調用的晚。
defer與return誰先誰後
return先,defer後
這個可能會讓人懷疑,後面會詳細解釋。
函數的傳回與return
在沒有defer的情況下,其實函數的傳回就是與return一緻的,但是有了defer就不一樣了。
函數的傳回其實是有兩個步驟的,第一個當執行到return語句的時候
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
複制
這個時候會先将傳回值t指派為2,然後執行defer,完成之後才會真正傳回外部調用者。
defer調用的三步走
這個就是今天的重頭戲了,defer這個文法其實一共有三個步驟。
- 将defer方法中的參數進行指派。
- 将defer壓入棧中。
-
當return或者是panic的時候依次出棧執行。
後面會用實際的例子說明具體執行的情況。
解釋
有了上面的所有知識點,其實你就應該能明白上面輸出的結果了。如果還不明白就看看下面的分析解釋吧。
DeferFunc1
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
複制
首先上面是第一個方法
- 将傳回值t指派為傳入的i,此時t為1
- 執行return語句将t指派給t(等于啥也沒做)
- 執行defer方法,将t + 3 = 4
-
函數傳回 4
因為t的作用域為整個函數是以修改有效。
DeferFunc2
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
複制
第二個方法
- 建立變量t并指派為1
- 執行return語句,注意這裡是将t指派給傳回值,此時傳回值為1(這個傳回值并不是t)
- 執行defer方法,将t + 3 = 4
- 函數傳回傳回值1
可能這裡就有點難了解了,修改一下代碼你就明白了
func DeferFunc2(i int) (result int) {
t := i
defer func() {
t += 3
}()
return t
}
複制
上面的代碼return的時候相當于将t指派給了result,當defer修改了t的值之後,對result是不會造成影響的。
DeferFunc3
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
複制
- 首先執行return将傳回值t指派為2
- 執行defer方法将t + 1
- 最後傳回 3
DeferFunc4
func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}
複制
這個分析的步驟要詳細一些
- 初始化傳回值t為零值 0
- 首先執行defer的第一步,指派defer中的func入參t為0
- 執行defer的第二步,将defer壓棧
- 将t指派為1
- 執行return語句,将傳回值t指派為2
-
執行defer的第三步,出棧并執行
因為在入棧時defer執行的func的入參已經指派了,此時它作為的是一個形式參數,是以列印為0;相對應的因為最後已經将t的值修改為2,是以再列印一個2
源碼一瞥
那麼 defer 在底層究竟是如何實作的呢?
通過生成彙編代碼我們可以看到下面這樣的方法:
CALL runtime.deferproc(SB)
CALL runtime.deferreturn(SB)
實際上來說當我們使用defer的使用就會調用runtime.deferproc,那麼這個時候,就會将所有的參數指派好,所有就像我們上面例子中看到的一樣,在調用defer的時候參數會先計算好儲存起來,然後挂載到G._defer中,最後deferreturn的時候進行執行相關的defer中的方法
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
sp := getcallersp(unsafe.Pointer(&siz))
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc(unsafe.Pointer(&siz))
systemstack(func() {
d := newdefer(siz)
})
d.fn = fn
d.pc = callerpc
d.sp = sp
memmove(add(unsafe.Pointer(d), unsafe.Sizeof(*d)),
unsafe.Pointer(argp), uintptr(siz))
// deferproc returns 0 normally.
// a deferred func that stops a panic makes the deferproc return 1.
// the code the compiler generates always checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
}
複制
總結
看完,有的人肯定又要出來搞事了,說這個在實際中不會遇到的,實際中誰寫這麼蠢的代碼。但是其實某些時候非常重要,當我們需要在defer中傳回一些錯誤資訊的時候,并且需要将這些資訊給到調用者的時候,就需要注意變量的作用域以及執行順序所帶來的差異。
而且正因為這樣的執行順序,在實際中要記住:
defer 最大的功能是 panic 後依然有效
是以defer可以保證你的一些資源一定會被關閉,進而避免一些異常出現的問題。
參考例子來源于網絡,自己做了修改和結合:
https://stackoverflow.com/questions/52718143/is-golang-defer-statement-execute-before-or-after-return-statement
答案
4
1
3
2