Go語言中defer的一些坑

simpleapples發表於2018-09-14

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私房菜

相關文章