go defer溫柔陷阱

一、什么是defer?

defer是Go語言提供的一種用于注冊延遲調用的機制:讓函數或語句可以在當前函數執(zhí)行完畢后(包括通過return正常結束或者panic導致的異常結束)執(zhí)行。

defer語句通常用于一些成對操作的場景:打開連接/關閉連接;加鎖/釋放鎖;打開文件/關閉文件等。

defer在一些需要回收資源的場景非常有用,可以很方便地在函數結束前做一些清理操作。在打開資源語句的下一行,直接一句defer就可以在函數返回前關閉資源,可謂相當優(yōu)雅。

f, _ := os.Open("defer.txt")
defer f.Close()

注意:以上代碼,忽略了err, 實際上應該先判斷是否出錯,如果出錯了,直接return. 接著再判斷f是否為空,如果f為空,就不能調用f.Close()函數了,會直接panic的。

二、為什么需要defer?

程序員在編程的時候,經常需要打開一些資源,比如數據庫連接、文件、鎖等,這些資源需要在用完之后釋放掉,否則會造成內存泄漏。

但是程序員都是人,是人就會犯錯。因此經常有程序員忘記關閉這些資源。Golang直接在語言層面提供defer關鍵字,在打開資源語句的下一行,就可以直接用defer語句來注冊函數結束后執(zhí)行關閉資源的操作。因為這樣一顆“小小”的語法糖,程序員忘寫關閉資源語句的情況就大大地減少了。

三、怎樣合理使用defer?

defer的使用其實非常簡單:

f,err := os.Open("test.txt")

defer func() {
    if f != nil {
        f.Close()
    }
}()

if err != nil {
    panic(err)
}

在打開文件的語句附近,用defer語句關閉文件。這樣,在函數結束之前,會自動執(zhí)行defer后面的語句來關閉文件。

當然,defer會有小小地延遲,對時間要求特別特別特別高的程序,可以避免使用它,其他一般忽略它帶來的延遲。

四、defer進階

4.1 defer的底層原理是什么?

我們先看一下官方對defer的解釋:

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.

翻譯一下:每次defer語句執(zhí)行的時候,會把函數“壓棧”,函數參數會被拷貝下來;當外層函數(非代碼塊,如一個for循環(huán))退出時,defer函數按照定義的逆序執(zhí)行;如果defer執(zhí)行的函數為nil, 那么會在最終調用函數的產生panic.

defer語句并不會馬上執(zhí)行,而是會進入一個棧,函數return前,會按先進后出的順序執(zhí)行。也說是說最先被定義的defer語句最后執(zhí)行。先進后出的原因是后面定義的函數可能會依賴前面的資源,自然要先執(zhí)行;否則,如果前面先執(zhí)行,那后面函數的依賴就沒有了。

在defer函數定義時,對外部變量的引用是有兩種方式的,分別是作為函數參數和作為閉包引用。

  • 作為函數參數,則在defer定義時就把值傳遞給defer,并被cache起來;
  • 作為閉包引用,則會在defer函數真正調用時根據整個上下文確定當前的值。

defer后面的語句在執(zhí)行的時候,函數調用的參數會被保存起來,也就是復制了一份。真正執(zhí)行的時候,實際上用到的是這個復制的變量,因此如果此變量是一個“值”,那么就和定義的時候是一致的。如果此變量是一個“引用”,那么就可能和定義的時候不一致。

func main() {
    var whatever [3]struct{}
    
    for i := range whatever {
        defer func() { 
            fmt.Println(i) 
        }()
    }
}

執(zhí)行結果:

2
2
2

defer后面跟的是一個閉包,i是“引用”類型的變量,最后i的值為2, 因此最后打印了三個2.

有了上面的基礎,我們來檢驗一下成果:

type number int

func (n number) print()   { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }

func main() {
    var n number

    defer n.print()
    defer n.pprint()
    defer func() { n.print() }()
    defer func() { n.pprint() }()

    n = 3
}

執(zhí)行結果是:

3
3
3
0

第四個defer語句是閉包,引用外部函數的n, 最終結果是3;
第三個defer語句同第四個;
第二個defer語句,n是引用,最終求值是3.
第一個defer語句,對n直接求值,開始的時候n=0, 所以最后是0;

4.2 defer命令的拆解

以一條語句為例:

return xxx

上面這條語句經過編譯之后,變成了三條指令:

1. 返回值 = xxx
2. 調用defer函數
3. 空的return

1,3步才是Return 語句真正的命令,第2步是defer定義的語句,這里可能會操作返回值。

下面我們來看兩個例子,試著將return語句和defer語句拆解到正確的順序。

第一個例子:

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

拆解后:

func f() (r int) {
     t := 5
     
     // 1. 賦值指令
     r = t
     
     // 2. defer被插入到賦值與返回之間執(zhí)行,這個例子中返回值r沒被修改過
     func() {        
         t = t + 5
     }
     
     // 3. 空的return指令
     return
}

這里第二步沒有操作返回值r, 因此,main函數中調用f()得到5.

第二個例子:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

拆解后:

func f() (r int) {
     // 1. 賦值
     r = 1
     
     // 2. 這里改的r是之前傳值傳進去的r,不會改變要返回的那個r值
     func(r int) { 
          r = r + 5
     }(r)
     
     // 3. 空的return
     return
}

因此,main函數中調用f()得到1.

4.3 defer語句的參數

defer語句表達式的值在定義時就已經確定了。下面展示三個函數:

func f1() {
    var err error
    
    defer fmt.Println(err)

    err = errors.New("defer error")
    return
}

func f2() {
    var err error
    
    defer func() {
        fmt.Println(err)
    }()

    err = errors.New("defer error")
    return
}

func f3() {
    var err error
    
    defer func(err error) {
        fmt.Println(err)
    }(err)

    err = errors.New("defer error")
    return
}

func main() {
    f1()
    f2()
    f3()
}

運行結果:

<nil>
defer error
<nil>

第1,3個函數是因為作為函數參數,定義的時候就會求值,定義的時候err變量的值都是nil, 所以最后打印的時候都是nil. 第2個函數的參數其實也是會在定義的時候求值,只不過,第2個例子中是一個閉包,它引用的變量err在執(zhí)行的時候最終變成defer error了。關于閉包在本文后面有介紹。

第3個函數的錯誤還比較容易犯,在生產環(huán)境中,很容易寫出這樣的錯誤代碼。最后defer語句沒有起到作用。

4.4 defer配合recover

panic會停掉當前正在執(zhí)行的程序,不只是當前協(xié)程。在這之前,它會有序地執(zhí)行完當前協(xié)程defer列表里的語句,其它協(xié)程里掛的defer語句不作保證。因此,我們經常在defer里掛一個recover語句,防止程序直接掛掉,這起到了try...catch的效果。

注意,recover()函數只在defer的上下文中才有效(且只有通過在defer中用匿名函數調用才有效),直接調用的話,只會返回nil.

func main() {
    defer fmt.Println("defer main")
    var user = os.Getenv("USER_")
    
    go func() {
        defer func() {
            fmt.Println("defer caller")
            if err := recover(); err != nil {
                fmt.Println("recover success. err: ", err)
            }
        }()

        func() {
            defer func() {
                fmt.Println("defer here")
            }()

            if user == "" {
                panic("should set user env.")
            }

            // 此處不會執(zhí)行
            fmt.Println("after panic")
        }()
    }()

    time.Sleep(100)
    fmt.Println("end of main function")
}

panic最終會被recover捕獲到,穩(wěn)住主進程,不影響整體服務。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容