Golang中多用途的defer

defer顧名思義就是延遲執行,那麼defer在Golang中該如何使用以及何時使用呢?

A "defer" statement invokes a function whose executionis deferred to the moment the surrounding function returns,

Golang的官方時這麼定義的。

1.那麼在什麼情況下會調用defer延遲過的函數呢?

從文檔中可以知道主要有兩種情況:

  1. 當函數執行了return 語句後

  2. 當函數處於panicing狀態,也就是程序中panic回溯上來到當前函數範圍內時

2. 如何使用defer呢?

defer可以當作一個修飾符,其不是用來修飾函數定義的,而是用來修飾函數調用的。當你調用一個函數時,在正常的書寫方式的前面加上 defer 之後,該函數調用就被延遲了,具體我們來看個例子:

func TestPanic(){
    defer func (){fmt.Println("defer function be invoked")}()
    fmt.Println("The first one to be invoke")
    return
}

運行後會發現結果爲:

The first one to be invoke
defer function be invoked

這裏我們用到了一個匿名函數,但是其原型就是 fun() 形式的調用。這裏符合上面的在return 語句之後執行了我們defer過的函數。

3.defer主要用於什麼場景

defer最主要的使用場景有兩個,一個時資源的釋放,如關閉打開的文件,另一個是和panic 以及 recover 組合起來模擬try...catch 功能;。除此之外defer還可以用來對return命名返回值作修改。

資源釋放當我們打開文件作操作後,很容易忘記釋放文件資源。假設現在B資源要依賴A資源。如下:

resA, err:= getA()
if err!=nil {
    return
}
resB,err := getB(resA)
if err != nile {
    return
}

這裏我們就容易產生bug,在獲取B資源出錯時,沒有釋放獲取成功的A資源。這裏我們添加:

resA, err:= getA()
defer ReleaseA()
if err!=nil {
    return
}
resB,err := getB(resA)
defer ReleaseB()
if err != nile {
    return
}

就可以保證,無論時在哪個位置返回,其上的資源都會得到釋放。

  • 異常處理defer還有最大的用處就是和panic以及recover組合成try...catch結構。

看個示例,來自官網的:

func protect(g func()) {
    defer func() {
        log.Println("done")  // Println executes normally even if there is a panic
        if x := recover(); x != nil {
            log.Printf("run time panic: %v", x)
        }
    }()
    log.Println("start")
    g()
}

當g()中通過panic拋出錯誤時,會在defer中用recover進行捕獲。也就是在子函數中的panic觸發了其處於panicing狀態,從而當panic回溯到當前函數時調用本函數的defer修飾的函數。

當然這裏把g()替換成panic()也是可行的,就沒有panic回溯了,直接時本函數中的panic觸發使其處在panicing狀態,從而調用defer修飾的函數。

  • 修改返回值

如同上面的例子,如果對於有異常和沒有異常返回不同的值,那麼該如何操作。傳統的方法是:

try {
    ...
} catch (exception1){
    return 1;
}
return 2
...

由於Golang中不能在defer裏面返回值,所以我們不能用上面的邏輯,在主函數和defer裏面返回不同的結果。這個時候我們可以藉助命名返回值來幫忙。在Golang中使用

func InvokeF()(rst int){
    defer func(){
        if err:=recover();err!=nil {
            if err== exception1 {
                rst = -1
            }
        }
    }()
    rst = f()
    return
}

當正常時,返回的rst爲f()調用結果,當出現異常時,rst的值會被修改,從而達到目的。

4.defer中的坑

  • 棧式調用

defer的調用順序時棧式的,也就是後修飾的函數會被先調用。如果熟悉進程和線程API的化,這裏defer的調用就和at_exit()有點類似,在退出時棧式調用註冊過的退出函數。在使用中我們會發現這樣的方式是符合我們代碼原意的。假設A資源依賴B資源。那麼我們這樣:

resA ,err := getA()
defer releaseA()
...
resB,err := getB(resA)
defer releaseB()

一來,我們在獲取資源時就註冊defer,語義上跟符合人類語言。二來,當A資源獲取失敗時僅釋放A的環境;當B資源獲取失敗時,釋放B和A的資源,且先釋放依賴A的B。這樣代碼更爲整潔易讀。

  • 出現時計算

首先要注意的是,defer修飾的函數調用中如果有參數,那麼參數取的是當前計算時的值,而不是在return或者panic時該變量的值。

func main() {
    i := 2;
    deferFun := func (i int){fmt.Printf("i in panic is %d \n",i)}
    defer deferFun(i+1)
    i += 12
    fmt.Printf("i befor return is %d \n",i)
    return
}

得到結果爲:

ibeforreturnis14iinpanicis3

注意,這裏只是執行了參數的計算,而並沒有執行函數體,函數體時在return之後執行的。

  • 丟棄返回值

defer修飾過的函數調用的返回值時丟棄的,因此不要想着再去使用其返回值。本來也沒有地方去使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章