避免defer陷阱:拆解延遲語句,掌握正確使用方法

2023-11-16 12:00:37

基本概念

Go語言的延遲語句defer有哪些特點?通常在什麼情況下使用?

Go語言的延遲語句(defer statement)具有以下特點:

  1. 延遲執行:延遲語句會在包含它的函數執行結束前執行,無論函數是正常返回還是發生異常。

  2. 後進先出:如果有多個延遲語句,它們會按照後進先出(LIFO)的順序執行。也就是說,最後一個延遲語句會最先執行,而第一個延遲語句會最後執行。

通常情況下,延遲語句在以下情況下使用:

  1. 資源釋放:延遲語句可以用於在函數返回前釋放開啟的檔案、關閉資料庫連線、釋放鎖等資源,以確保資源的正確釋放,避免資源洩漏。

  2. 錯誤處理:延遲語句可以用於處理常式執行過程中可能發生的錯誤。通過在函數開始時設定延遲語句,在函數返回前檢查錯誤並進行相應的處理,可以簡化錯誤處理的邏輯。

  3. 紀錄檔記錄:延遲語句可以用於在函數返回前記錄紀錄檔或執行其他的偵錯操作,以便在函數執行過程中收集相關的資訊。

延遲語句的使用可以提高程式碼的可讀性和可維護性,同時確保資源的釋放和清理操作按照逆序進行。它是Go語言中一種常用的程式設計技巧,用於處理資源管理和錯誤處理等場景。

避坑之旅

實際開發中defer的使用並不像前面介紹的這麼簡單,defer用不好,會陷入泥潭。

下面我從兩個角度帶大家避坑:

  1. 首先拆解一下延遲語句的執行,注意Go語言的return語句不是原子性的;

  2. 另外重點和大家分享一下defer語句後面使用匿名函數和非匿名函數的區別。

拆解延遲語句

避免陷入泥潭的關鍵是必須深刻理解下面這條語句:

return xxx

上面這條語句經過編譯之後,實際上生成了三條指令:

1)返回值 =xxx。

2)呼叫 defer 函數。

3)空的 return。

第1和第 3 步是return語句生成的指令,也就是說return並不是一條原子指令;

第2步是 defer 定義的語句,這裡可能會操作返回值,從而影響最終結果。

下面來看兩個例子,試著將return 語句和 defer語句拆解到正確的順序。

第一個例子:

func f()(r int){
  t:=5

  defer func(){
    t=t+5
    }()
    
  return t
}

拆解後:

func f()(r int){
  t:=5
  
  //1,賦值指令
  r=t

  // 2.defer 被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過 
  func(){
    t=t+5
    }()
    
  //3.空的 return 指令
  return
  }

這裡第二步實際上並沒有操作返回值r,因此,main函數中呼叫f()得到5。

第二個例子:

func f()(r int){
  defer func(r int){
    r=r+5
    }(r)
    
    return 1
}

拆解後:

func f() (r int) {
  //1.賦值 
  r=1
  
  //2.這裡改的r是之前傳進去的r,不會改變要返回的那個r值 
  func(r int) {
    r=r+5
  }(r)
  
  // 3. 空的 return 
  return
}

第二步,改變的是傳值進去的r,是形參的一個複製值,不會影響實參r。因此,main函數中需要呼叫f()得到1。

defer匿名函數

在Go語言中,使用匿名函數作為defer的引數時,可以理解為:defer語句中的匿名函數在包裹該defer語句的函數返回後才執行。這是因為defer語句的執行時機是在包裹函數即將返回之前,但在實際返回之前。

為什麼不是在return語句之前執行呢?這是因為defer語句的設計初衷是為了在函數返回之前執行一些清理操作,例如關閉檔案、釋放資源等。將defer語句放在return語句之後,可以確保在函數返回之前執行這些清理操作,保證函數的執行完整性和資源的正確釋放。

在使用匿名函數和非匿名函數作為defer的引數時,主要區別在於對函數引數的傳遞和作用域的影響:

  1. 匿名函數作為defer的引數:匿名函數可以直接在defer語句中定義,可以存取外部函數的變數,並且在執行時會使用當前的變數值。這種方式可以方便地在defer語句中使用外部變數,但需要注意變數的值在執行時可能已經發生了改變。

  2. 非匿名函數作為defer的引數:非匿名函數需要先定義好,然後作為defer的引數傳遞。在執行時,會使用函數的當前引數值。這種方式可以在defer語句中使用已定義的函數,但需要注意函數引數的傳遞和作用域。

產生這種區別的原因是,匿名函數和非匿名函數在定義和作用域上的差異。匿名函數可以直接在defer語句中定義,可以存取外部函數的變數,而非匿名函數需要先定義好,然後作為引數傳遞。這種設計靈活性使得開發者可以根據具體的需求選擇合適的方式來使用defer語句。

舉例來說

當使用匿名函數作為defer的引數時,可以在defer語句中直接定義匿名函數,並存取外部變數。

以下是一個範例程式碼:

package main

import "fmt"

func main() {
    x := 10

    defer func() {
        fmt.Println("Deferred anonymous function:", x)
    }()

    x = 20
    fmt.Println("Before return:", x)
}

在上述範例中,匿名函數作為defer的引數,可以存取外部變數x
在函數返回之前,defer語句中的匿名函數會執行,並列印出x的值。

輸出結果如下:

當使用非匿名函數作為defer的引數時,需要先定義好函數,然後將函數名作為defer的引數傳遞。

以下是一個範例程式碼:

package main

import "fmt"

func main() {
    x := 10

    defer printX(x)

    x = 20
    fmt.Println("Before return:", x)
}

func printX(x int) {
    fmt.Println("Deferred function:", x)
}

在上述範例中,printX函數作為defer的引數傳遞,函數定義在main函數之後。

在函數返回之前,defer語句中的printX函數會執行,並列印出傳遞的引數x的值。輸出結果如下:

總結一下

通過以上範例,我們可以明確體現出使用匿名函數和非匿名函數作為defer的引數的區別。

匿名函數可以直接在defer語句中定義,並存取外部變數,而非匿名函數需要先定義好函數,然後將函數名作為引數傳遞。

通過前面帶著大家拆解了defer的語句的執行,相信大家可以更好的理解了。

更多defer使用的技巧和踩坑經驗,歡迎在評論區交流討論。
歡迎加我微信:wangzhongyang1993