1. 程式人生 > >Go 延遲函數 defer 詳解

Go 延遲函數 defer 詳解

無法 eight order 理解 start 如果 div fmt 方式

Go 延遲函數 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)
    }

Go 延遲函數 defer 詳解