Go語言中defer的一些坑

defer語句是Go中一個非常有用的特性,可以將一個方法延遲到包裹該方法的方法返回時執行,在實際應用中,defer語句可以充當其他語言中try…catch…的角色,也可以用來處理關閉文件句柄等收尾操作。

defer觸發時機

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

Go官方文檔中對defer的執行時機做了闡述,分別是。

  • 包裹defer的函數返回時
  • 包裹defer的函數執行到末尾時
  • 所在的goroutine發生panic時

defer執行順序

當一個方法中有多個defer時, defer會將要延遲執行的方法“壓棧”,當defer被觸發時,將所有“壓棧”的方法“出棧”並執行。所以defer的執行順序是LIFO的。

所以下面這段代碼的輸出不是1 2 3,而是3 2 1。

func stackingDefers() {
    defer func() {
        fmt.Println("1")
    }()
    defer func() {
        fmt.Println("2")
    }()
    defer func() {
        fmt.Println("3")
    }()
}

坑1:defer在匿名返回值和命名返回值函數中的不同表現

先看下面兩個方法執行的結果。

func returnValues() int {
    var result int
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

func namedReturnValues() (result int) {
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

上面的方法會輸出0,下面的方法輸出1。上面的方法使用了匿名返回值,下面的使用了命名返回值,除此之外其他的邏輯均相同,爲什麼輸出的結果會有區別呢?

要搞清這個問題首先需要了解defer的執行邏輯,文檔中說defer語句在方法返回“時”觸發,也就是說return和defer是“同時”執行的。以匿名返回值方法舉例,過程如下。

  • 將result賦值給返回值(可以理解成Go自動創建了一個返回值retValue,相當於執行retValue = result)
  • 然後檢查是否有defer,如果有則執行
  • 返回剛纔創建的返回值(retValue)

在這種情況下,defer中的修改是對result執行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由於返回值在方法定義時已經被定義,所以沒有創建retValue的過程,result就是retValue,defer對於result的修改也會被直接返回。

坑2:在for循環中使用defer可能導致的性能問題

看下面的代碼

func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

defer在緊鄰創建資源的語句後生命力,看上去邏輯沒有什麼問題。但是和直接調用相比,defer的執行存在着額外的開銷,例如defer會對其後需要的參數進行內存拷貝,還需要對defer結構進行壓棧出棧操作。所以在循環中定義defer可能導致大量的資源開銷,在本例中,可以將f.Close()語句前的defer去掉,來減少大量defer導致的額外資源消耗。

坑3:判斷執行沒有err之後,再defer釋放資源

一些獲取資源的操作可能會返回err參數,我們可以選擇忽略返回的err參數,但是如果要使用defer進行延遲釋放的的話,需要在使用defer之前先判斷是否存在err,如果資源沒有獲取成功,即沒有必要也不應該再對資源執行釋放操作。如果不判斷獲取資源是否成功就執行釋放操作的話,還有可能導致釋放方法執行錯誤。

正確寫法如下。

resp, err := http.Get(url)
// 先判斷操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再進行Close操作
defer resp.Body.Close()

坑4:調用os.Exit時defer不會被執行

當發生panic時,所在goroutine的所有defer會被執行,但是當調用os.Exit()方法退出程序時,defer並不會被執行。

func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

上面的defer並不會輸出。

點擊關注知乎專欄Golang私房菜

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