Go defer 特性和使用場景


golang 的 defer 語句用於延遲調用。defer 會在當前函數返回之前執行 defer 注冊的函數。比如 defer func_defer() 這樣語句會讓你注冊一個函數變量到 defer 的全局鏈表中,在 defer 語句所在的函數退出之前調用。

defer 可以代替其它語言中 try…catch… 語句,也可以用來處理釋放資源等收尾操作,比如關閉文件句柄、關閉數據庫連接等。defer 還能用於 panic 的 recovery。

1. defer 的特性

我們先深入的剖析下 defer 具有的特性,知其然也。這些特性是需要我們記住的特點,才能更好的理解 defer 使用的場景。

1) 延遲調用

package main

func main() {
    defer println("--- defer ---") println("--- end ---") }

運行結果:

--- end ---
--- defer ---

defer 會在 main 函數所有語句之后, return 之前時候調用。核心要點:

  • 延遲調用:defer 語句本身雖然是 main 的第一行,但是 println("--- end ---") 先打印的;
  • defer 關鍵字一定是處於函數上下文:defer 必須放在函數內部;

2) LIFO

一個函數中含有有多個 defer,調用順序采用壓棧式執行,后入先出(LIFO)。

package main

import (
    "strconv" ) func main() { for i := 1; i <= 3; i++ { defer println("defer -->" + strconv.Itoa(i)) } println("--- end ---") }

壓棧式執行,也就是說先注冊的函數后調用。如上,我們注冊的順序式 1,2,3,最后打印 “--- end ---”,所以執行的結果自然是反着來的,程序輸出:

--- end ---
defer -->3 defer -->2 defer -->1

3) 作用域

defer 只會和 defer 語句所在的特定函數綁定在一起,作用域也只在這個函數。 從語法上來講,defer 語句也一定要在函數內,否則會報告語法錯誤。

package main

func main() {
    func() {
        defer println("--- defer ---") }() println("--- end ---") }

如上,defer 處於一個匿名函數中,就 main 函數本身來講,匿名函數 fun(){}() 先調用且返回,然后再調用 println("--- end ---") ,所以程序輸出自然是:

--- defer ---
--- end ---

4) 異常場景

這個是非常重要的特性: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 結合才有意義;

 

2. defer 使用場景

1) 並發同步

以下的例子對兩個並發的協程做了下同步控制,常規操作。

var wg sync.WaitGroup

for i := 0; i < 2; i++ { wg.Add(1) go func() { defer wg.Done() // 程序邏輯  }() } wg.Wait()

2) 鎖場景

加鎖解鎖必須配套,在 golang 有了 defer 之后,你就可以寫了 lock 之后,立馬就寫 unlock ,這樣就永遠不會忘了。

mu.RLock()
defer mu.RUnlock()

但是請注意,lock 以下的代碼都會在鎖內。所以下面的代碼要足夠精簡和快速才行,如果說下面的邏輯很復雜,那么可能就需要手動控制 unlock 防止的位置了。

3) 資源釋放

某些資源是臨時創建的,作用域只存在於現場函數中,用完之后需要銷毀,這種場景也適用 defer 來釋放。釋放就在創建的下一行,這是個非常好的編程體驗,這種編程方式能極大的避免資源泄漏。因為寫了創建立馬就可以寫釋放了,再也不會忘記了。

// 創建一個客戶端 client;
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil { log.Fatal(err) } // 釋放該 client ,也就是說該 client 的聲明周期就只在該函數中; defer cli.Close()

4) panic-recover

recover 必須和 defer 結合才行,使用姿勢一般如下:

defer func() {
    if v := recover(); v != nil { _ = fmt.Errorf("PANIC=%v", v) } }() 

 

3. 總結

  • defer 其實並不是 golang 獨創,是多種高級語言的共同選擇;
  • defer 最重要的一個特點就是無視異常可執行,這個是 golang 在提供了 panic-recover 機制之后必須做的補償機制;
  • defer 的作用域存在於函數,defer 也只有和函數結合才有意義;
  • defer 允許你把配套的兩個行為代碼放在最近相鄰的兩行,比如創建&釋放資源、加鎖&釋放鎖等,使得代碼更易讀,編程體驗優秀。

參考資料

   1. go語言編程

 2. 編程寶庫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM