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並不會輸出。