你知道defer的坑嗎?

你是不是覺得defer很簡單、很好用,但也許你掉坑裏了都不知道!

這篇文章不介紹defer的常用功能,而是介紹你在用defer時,也許會踩的坑。

defer允許我們進行一些函數執行完成後的收尾工作,並且代碼更加簡潔,例如:

  1. 關閉文件流:

    // open a file
    defer file.Close()
  2. 解鎖一個加鎖的資源

    mu.Lock()
    defer mu.Unlock()
  3. 打印最終報告

    printHeader()
    defer printFooter()
  4. 關閉數據庫鏈接

    // open a database connection
    defer disconnectFromDB() 

但是:

  • 你知道defer和defer後的函數什麼時候執行嗎?
  • 你知道defer後函數裏的變量值是什麼時候計算的嗎?

我曾經在Stack Overflow討論過這個問題,有興趣的可以看下,本打算在週末寫個文章分享給大家defer的坑,今天不小心瀏覽到一個有誤解的文章,決定現在就寫下來,希望大家不要踩坑。

defer陷阱測試

如果下面這段代碼的結果你都知道,恭喜你,你已經瞭解defer的執行原理,沒有必要再看這篇文章了。

func test1() (x int) {
    defer fmt.Printf("in defer: x = %d\n", x)
    x = 7
    return 9
}

func test2() (x int) {
    x = 7
    defer fmt.Printf("in defer: x = %d\n", x)
    return 9
}

func test3() (x int) {
    defer func() {
        fmt.Printf("in defer: x = %d\n", x)
    }()

    x = 7
    return 9
}

func test4() (x int) {
    defer func(n int) {
        fmt.Printf("in defer x as parameter: x = %d\n", n)
        fmt.Printf("in defer x after return: x = %d\n", x)
    }(x)

    x = 7
    return 9
}

func main() {
    fmt.Println("test1")
    fmt.Printf("in main: x = %d\n", test1())
    fmt.Println("test2")
    fmt.Printf("in main: x = %d\n", test2())
    fmt.Println("test3")
    fmt.Printf("in main: x = %d\n", test3())
    fmt.Println("test4")
    fmt.Printf("in main: x = %d\n", test4())
}

你已經計算出結果了嗎?看看和運行結果是不是一樣的,如果不一樣繼續閱讀本文吧:

test1
in defer: x = 0
in main: x = 9
test2
in defer: x = 7
in main: x = 9
test3
in defer: x = 9
in main: x = 9
test4
in defer x as parameter: x = 0
in defer x after return: x = 9
in main: x = 9

defer執行原理

要想知道爲何是這個結果,就得先回答前面的2個問題:

  1. defer和defer後的函數什麼時候執行嗎?
  2. defer後函數裏的變量值是什麼時候計算的嗎?

依次來回答,這2個問題。

問題1:defer在defer語句處執行,defer的執行結果是把defer後的函數壓入到棧,等待return或者函數panic後,再按先進後出的順序執行被defer的函數。

問題2:defer的函數的參數是在執行defer時計算的,defer的函數中的變量的值是在函數執行時計算的。

defer及defer函數的執行順序分2步:

  1. 執行defer,計算函數的入參的值,並傳遞給函數,但不執行函數,而是將函數壓入棧。
  2. 函數return語句後,或panic後,執行壓入棧的函數,函數中變量的值,此時會被計算。

defer測試解析

這4個測試函數中,都是return 9並且沒有對返回值進行修改,所以main中都是in main: x = 9,我相信這個大家應該是沒有疑問的。接下來看每個測試函數defer的打印。

test1:defer執行時,對Printf的入參x進行計算,它的值是0,並且傳遞給函數,return 9後執行Printf,所以結果是in defer: x = 0

test2:與test1類似,不同僅是,defer執行是在x=7之後,所以x的值是7,並且傳遞給Printf,所以結果是:in defer: x = 7

test3:defer後跟的是一個匿名函數,匿名函數能訪問外部函數的變量,這裏訪問的是test3的x,defer執行時,匿名函數沒有入參,所以把func()()壓入到棧,return語句之後,執行func()(),此時匿名函數獲得x的值是9,所以結果是in defer: x = 9

test4:與test3的不同是,匿名函數有一個入參n,我們把x作爲入參打印,還有就是匿名函數訪問外部打印x。defer執行時,x=0,所以入棧的函數是func(int)(0),return語句之後執行func(int)(0),即n=0,x在匿名函數內沒有定義,依然訪問test4中的x,此時x=9,所以結果爲:in defer x as parameter: x = 0, in defer x after return: x = 9

誤解文章截圖

最後,看下誤解讀者文章的截圖,看看你能不能發現那篇文章作者的思路問題。

截圖1.png

截圖2.png

截圖3.png

上文的作者的目的想知道defer是在return之前,還是之後執行,所以做了這麼個測試,他把上面的代碼和修改成下面的代碼,發現等效後,就給出了錯誤結論:defer確實是在return之前調用的。

等效的能證明,順序嗎?請各位自行思考吧。

defer的核心

Golang對於defer的介紹)很精簡,但是把上面提到的問題都說清楚了,我也是讀了幾遍和其他人交流,才完全理解透,不妨好好讀讀,最核心的一句:

Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.

參考資料

如果這篇文章對你有幫助,請點個贊/喜歡,讓我知道我的寫作是有價值的,感謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章