天天看點

不是我吹,你可能連defer都不清楚

在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這個文法其實一共有三個步驟。

  1. 将defer方法中的參數進行指派。
  2. 将defer壓入棧中。
  3. 當return或者是panic的時候依次出棧執行。

    後面會用實際的例子說明具體執行的情況。

解釋

有了上面的所有知識點,其實你就應該能明白上面輸出的結果了。如果還不明白就看看下面的分析解釋吧。

DeferFunc1

func DeferFunc1(i int) (t int) {
    t = i
    defer func() {
        t += 3
    }()
    return t
}           

複制

首先上面是第一個方法

  1. 将傳回值t指派為傳入的i,此時t為1
  2. 執行return語句将t指派給t(等于啥也沒做)
  3. 執行defer方法,将t + 3 = 4
  4. 函數傳回 4

    因為t的作用域為整個函數是以修改有效。

DeferFunc2

func DeferFunc2(i int) int {
    t := i
    defer func() {
        t += 3
    }()
    return t
}           

複制

第二個方法

  1. 建立變量t并指派為1
  2. 執行return語句,注意這裡是将t指派給傳回值,此時傳回值為1(這個傳回值并不是t)
  3. 執行defer方法,将t + 3 = 4
  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
}           

複制

  1. 首先執行return将傳回值t指派為2
  2. 執行defer方法将t + 1
  3. 最後傳回 3

DeferFunc4

func DeferFunc4() (t int) {
    defer func(i int) {
        fmt.Println(i)
        fmt.Println(t)
    }(t)
    t = 1
    return 2
}           

複制

這個分析的步驟要詳細一些

  1. 初始化傳回值t為零值 0
  2. 首先執行defer的第一步,指派defer中的func入參t為0
  3. 執行defer的第二步,将defer壓棧
  4. 将t指派為1
  5. 執行return語句,将傳回值t指派為2
  6. 執行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