Golang 的 defer 是什麼?通俗來講就是延遲調用。defer 會在當前函數返回之前執行 defer 註冊的函數。比如 defer func_x( ) 這樣語句會讓你註冊一個函數變量到 defer 的全局鍊表中,在 defer 語句所在的函數退出之前調用。
筆者使用一段時間 Golang 之後,對 Golang defer 的理解認為作用有兩點:
panic 場景依然會被調用:這個是重要的一個特性,通常能簡化我們的代碼,確保無論任何場景,defer 的函數一定調用,通常用在鎖或者資源的釋放場景較多;配套的兩個行為代碼可以放在最近的位置:創建&釋放、加鎖&放鎖、前置&後置,使得代碼更易讀,編程體驗優秀。最近的地方是哪裡?下一行;先看下 defer 的以下幾個特性:
我們先深入的剖析下 defer 具有的特性,知其然也。這些特性是需要我們記住的特點,才能更好的理解 defer 使用的場景。
defer 會在 main 函數 return 之前時候調用。核心要點:
延遲調用:defer 語句本身雖然是 main 的第一行,但是 fmt.Println 是先列印的;defer 關鍵字一定是處於函數上下文:defer 必須放在函數內部;一個函數中有多個 defer 調用怎麼辦?壓棧式執行,後入先出。
package main
import (
"strconv"
)
func main() {
for i := 1; i <= 6; i++ {
defer println("defer -->" + strconv.Itoa(i))
}
println("--- end ---")
}壓棧式執行,也就是說先註冊的函數後調用。如上,我們註冊的順序式 1,2,3,4,5,6,最後列印 "--- end ---",所以執行的結果自然是反著來的,程序輸出:
--- end ---
defer -->6
defer -->5
defer -->4
defer -->3
defer -->2
defer -->1要點:defer 和函數綁定。 兩個理解,defer 只會和 defer 語句所在的特定函數綁定在一起,作用域也只在這個函數。從語法上來講,defer 語句也一定要在函數內,否則會報告語法錯誤。
package main
func main() {
func() {
defer println("--- defer ---")
}()
println("--- ending ---")
}如上,defer 處於一個匿名函數中,就 main 函數本身來講,匿名函數 fun(){}() 先調用且返回,然後再調用 println("--- ending ---") ,所以程序輸出自然是:
--- defer ---
--- ending ---這個是個非常重要的特性:panic 也能執行。Golang 不鼓勵異常的編程模式,但是卻也留了 panic-recover 這個異常和捕捉異常的機制。所以 defer 機制就顯得尤為重要,甚至可以說是必不可少的。因為你沒有一個無視異常,永保調用的 defer 機制,很有可能就會發生各種資源洩露,死鎖等場景。為什麼?因為發生了 panic 卻不代表進程一定會掛掉,很有可能被外層 recover 住。
package main
func main() {
defer func() {
if e := recover(); e != nil {
println("--- defer ---")
}
}()
panic("throw panic")
}如上,main 函數註冊一個 defer ,且稍後主動觸發 panic,main 函數退出之際就會調用 defer 註冊的匿名函數。再提一點,這裡其實有兩個要點:
defer 在 panic 異常場景也能確保調用;recover 必須和 defer 結合才有意義;
以下的例子對兩個並發的協程做了下同步控制,常規操作。
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 程序邏輯
}()
}
wg.Wait()加鎖解鎖必須配套,在 Golang 有了 defer 之後,你就可以寫了 lock 之後,立馬就寫 unlock ,這樣就永遠不會忘了。
mu.RLock()
defer mu.RUnlock()但是請注意,lock 以下的代碼都會在鎖內。所以下面的代碼要足夠精簡和快速才行,如果說下面的邏輯很複雜,那麼可能就需要手動控制 unlock 防止的位置了。
某些資源是臨時創建的,作用域只存在於現場函數中,用完之後需要銷毀,這種場景也適用 defer 來釋放。釋放就在創建的下一行,這是個非常好的編程體驗,這種編程方式能極大的避免資源洩漏。因為寫了創建立馬就可以寫釋放了,再也不會忘記了。
// new 一個客戶端 client;
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
// 釋放該 client ,也就是說該 client 的聲明周期就只在該函數中;
defer cli.Close()recover 必須和 defer 結合才行,使用姿勢一般如下:
defer func() {
if v := recover(); v != nil {
_ = fmt.Errorf("PANIC=%v", v)
}
}()defer 其實並不是 Golang 獨創,是多種高級語言的共同選擇;defer 最重要的一個特點就是無視異常可執行,這個是 Golang 在提供了 panic-recover 機制之後必須做的補償機制;defer 的作用域存在於函數,defer 也只有和函數結合才有意義;defer 允許你把配套的兩個行為代碼放在最近相鄰的兩行,比如創建&釋放、加鎖&放鎖、前置&後置,使得代碼更易讀,編程體驗優秀;
本篇從 defer 的使用姿勢入手,了解 defer 的特性,讓大家知其然也。後續會從源碼和實現的角度出發,梳理下 defer ,然後知其所以然也。