天天看點

goang defer延遲函數詳解

作者:幹飯人小羽
goang defer延遲函數詳解

Go 語言中延遲函數 defer 充當着 try...catch 的重任,使用起來也非常簡便,然而在實際應用中,很多 gopher 并沒有真正搞明白 defer、return、傳回值、panic 之間的執行順序,進而掉進坑中,今天我們就來揭開它的神秘面紗!

先來運作下面兩段代碼:

A. 匿名傳回值的情況

package main

import (
    "fmt"
)

func main() {
    fmt.Println("a return:", a()) // 列印結果為 a return: 0
}

func a() int {
    var i int
    defer func() {
        i++
        fmt.Println("a defer2:", i) // 列印結果為 a defer2: 2
    }()
    defer func() {
        i++
        fmt.Println("a defer1:", i) // 列印結果為 a defer1: 1
    }()
    return i
}           

B. 有名傳回值的情況

package main

import (
    "fmt"
)

func main() {
    fmt.Println("b return:", b()) // 列印結果為 b return: 2
}

func b() (i int) {
    defer func() {
        i++
        fmt.Println("b defer2:", i) // 列印結果為 b defer2: 2
    }()
    defer func() {
        i++
        fmt.Println("b defer1:", i) // 列印結果為 b defer1: 1
    }()
    return i // 或者直接 return 效果相同
}           

先來假設出結論(這是正确結論),幫助大家了解原因:

  1. 多個 defer 的執行順序為“後進先出/先進後出”;
  2. 所有函數在執行 RET 傳回指令之前,都會先檢查是否存在 defer 語句,若存在則先逆序調用 defer 語句進行收尾工作再退出傳回;
  3. 匿名傳回值是在 return 執行時被聲明,有名傳回值則是在函數聲明的同時被聲明,是以在 defer 語句中隻能通路有名傳回值,而不能直接通路匿名傳回值;
  4. return 其實應該包含前後兩個步驟:第一步是給傳回值指派(若為有名傳回值則直接指派,若為匿名傳回值則先聲明再指派);第二步是調用 RET 傳回指令并傳入傳回值,而 RET 則會檢查 defer 是否存在,若存在就先逆序插播 defer 語句,最後 RET 攜帶傳回值退出函數;

是以,‍‍defer、return、傳回值三者的執行順序應該是:return最先給傳回值指派;接着 defer 開始執行一些收尾工作;最後 RET 指令攜帶傳回值退出函數。

如何解釋兩種結果的不同:

上面兩段代碼的傳回結果之是以不同,其實從上面的結論中已經很好了解了。

  • a()int 函數的傳回值沒有被提前聲名,其值來自于其他變量的指派,而 defer 中修改的也是其他變量(其實該 defer 根本無法直接通路到傳回值),是以函數退出時傳回值并沒有被修改。
  • b()(i int) 函數的傳回值被提前聲名,這使得 defer 可以通路該傳回值,是以在 return 指派傳回值 i 之後,defer 調用傳回值 i 并進行了修改,最後緻使 return 調用 RET 退出函數後的傳回值才會是 defer 修改過的值。

C. 下面我們再來看第三個例子,驗證上面的結論:

package main

import (
    "fmt"
)

func main() {
    c:=c()
    fmt.Println("c return:", *c, c) // 列印結果為 c return: 2 0xc082008340
}

func c() *int {
    var i int
    defer func() {
        i++
        fmt.Println("c defer2:", i, &i) // 列印結果為 c defer2: 2 0xc082008340
    }()
    defer func() {
        i++
        fmt.Println("c defer1:", i, &i) // 列印結果為 c defer1: 1 0xc082008340
    }()
    return &i
}           

雖然 c()int 的傳回值沒有被提前聲明,但是由于 c()int 的傳回值是指針變量,那麼在 return 将變量 i 的位址賦給傳回值後,defer 再次修改了 i 在記憶體中的實際值,是以 return 調用 RET 退出函數時傳回值雖然依舊是原來的指針位址,但是其指向的記憶體實際值已經被成功修改了。

即,我們假設的結論是正确的!

D. 補充一條,defer聲明時會先計算确定參數的值,defer推遲執行的僅是其函數體。

package main

import (
    "fmt"
    "time"
)

func main() {
    defer P(time.Now())
    time.Sleep(5e9)
    fmt.Println("main ", time.Now())
}

func P(t time.Time) {
    fmt.Println("defer", t)
    fmt.Println("P    ", time.Now())
}

// 輸出結果:
// main  2017-08-01 14:59:47.547597041 +0800 CST
// defer 2017-08-01 14:59:42.545136374 +0800 CST
// P     2017-08-01 14:59:47.548833586 +0800 CST           

E. defer 的作用域

  1. defer 隻對目前協程有效(main 可以看作是主協程);
  2. 當任意一條(主)協程發生 panic 時,會執行目前協程中 panic 之前已聲明的 defer;
  3. 在發生 panic 的(主)協程中,如果沒有一個 defer 調用 recover()進行恢複,則會在執行完最後一個已聲明的 defer 後,引發整個程序崩潰;
  4. 主動調用 os.Exit(int) 退出程序時,defer 将不再被執行。
package main

import (
    "errors"
    "fmt"
    "time"
    // "os"
)

func main() {
    e := errors.New("error")
    fmt.Println(e)
    // (3)panic(e) // defer 不會執行
    // (4)os.Exit(1) // defer 不會執行
    defer fmt.Println("defer")
    // (1)go func() { panic(e) }() // 會導緻 defer 不會執行
    // (2)panic(e) // defer 會執行
    time.Sleep(1e9)
    fmt.Println("over.")
    // (5)os.Exit(1) // defer 不會執行
}           

F. defer 表達式的調用順序是按照先進後出的方式執行

defer 表達式會被放入一個類似于棧( stack )的結構,是以調用的順序是先進後出/後進先出的。

下面這段代碼輸出的結果是 4321 而不是 1234 。

package main

import (
    "fmt"
)

func main() {
    defer fmt.Print(1)
    defer fmt.Print(2)
    defer fmt.Print(3)
    defer fmt.Print(4)
}           

分類: golang