defer和go同樣都是Go語言提供的關鍵字。defer用於資源的釋放,會在函數返回以前進行調用。通常採用以下模式:golang
f,err := os.Open(filename) if err != nil { panic(err) } defer f.Close()
若是有多個defer表達式,調用順序相似於棧,越後面的defer表達式越先被調用。c#
不過若是對defer的瞭解不夠深刻,使用起來可能會踩到一些坑,尤爲是跟帶命名的返回參數一塊兒使用時。在講解defer的實現以前先看一看使用defer容易遇到的問題。函數
先來看看幾個例子。例1:code
func f() (result int) { defer func() { result++ }() return 0 }
例2:資源
func f() (r int) { t := 5 defer func() { t = t + 5 }() return t }
例3:文檔
func f() (r int) { defer func(r int) { r = r + 5 }(r) return 1 }
請讀者先不要運行代碼,在內心跑一遍結果,而後去驗證。get
例1的正確答案不是0,例2的正確答案不是10,若是例3的正確答案不是6......io
defer是在return以前執行的。這個在 官方文檔中是明確說明了的。要使用defer時不踩坑,最重要的一點就是要明白,return xxx這一條語句並非一條原子指令!class
函數返回的過程是這樣的:先給返回值賦值,而後調用defer表達式,最後纔是返回到調用函數中。file
defer表達式可能會在設置函數返回值以後,在返回到調用函數以前,修改返回值,使最終的函數返回值與你想象的不一致。
其實使用defer時,用一個簡單的轉換規則改寫一下,就不會迷糊了。改寫規則是將return語句拆成兩句寫,return xxx會被改寫成:
返回值 = xxx 調用defer函數 空的return
先看例1,它能夠改寫成這樣:
func f() (result int) { result = 0 //return語句不是一條原子調用,return xxx實際上是賦值+ret指令 func() { //defer被插入到return以前執行,也就是賦返回值和ret指令之間 result++ }() return }
因此這個返回值是1。
再看例2,它能夠改寫成這樣:
func f() (r int) { t := 5 r = t //賦值指令 func() { //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過 t = t + 5 } return //空的return指令 }
因此這個的結果是5。
最後看例3,它改寫後變成:
func f() (r int) { r = 1 //給返回值賦值 func(r int) { //這裏改的r是傳值傳進去的r,不會改變要返回的那個r值 r = r + 5 }(r) return //空的return }
因此這個例子的結果是1。
defer確實是在return以前調用的。但表現形式上卻可能不像。本質緣由是return xxx語句並非一條原子指令,defer被插入到了賦值 與 ret之間,所以可能有機會改變最終的返回值。
defer關鍵字的實現跟go關鍵字很相似,不一樣的是它調用的是runtime.deferproc而不是runtime.newproc。
在defer出現的地方,插入了指令call runtime.deferproc,而後在函數返回以前的地方,插入指令call runtime.deferreturn。
普通的函數返回時,彙編代碼相似:
add xx SP return
若是其中包含了defer語句,則彙編代碼是:
call runtime.deferreturn, add xx SP return
goroutine的控制結構中,有一張表記錄defer,調用runtime.deferproc時會將須要defer的表達式記錄在表中,而在調用runtime.deferreturn的時候,則會依次從defer表中出棧並執行。