理解 defer、panic 和 recover

在 Go 語言中,有很多流程控制的語句,if、else 等等,這些流程控制的關鍵字在其他語言中也存在的但 Go 中還有幾個特殊的流程控制關鍵字,defer、panic 和 recover。

  1. defer

defer 可以保證一些代碼在函數或者方法返回之前被調用,即使方法沒有正常執行完,發生了 panic,defer 後面的代碼也會執行。這裏需要注意,不是在退出某個作用域之前會被調用,而且函數或者方法

defer 通常用來回收一些資源,比如關閉文件,關閉數據庫連接以及釋放一些資源(釋放鎖)。在使用 defer 的時候,有一些需要注意的地方。

  1. 參數預計算

在定義 defer 的時候,引用的外部參數會立刻被拷貝,在 i++ 執行之前就已經確定了,下面的代碼最後打印的值是 0, 而不是 1:

func t1() {
  i := 0
  defer fmt.Println(i)
  i++
}

如果想要最後打印的值是 1,則做如下修改:

func t2() {
  i := 0
  defer func() {
    fmt.Println(i)
  }()
  i++
}
  1. 如果定義了多個 defer 語句,最後定義的最先執行

下面的代碼輸出的結果是 4 3 2 1:

func t3() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  defer fmt.Println(3)
  defer fmt.Println(4)
}
  1. defer 可以對方法或者函數的命名返回值進行賦值

在下面的代碼中,正常情況下回返回 1,使用 defer 卻可以對返回值繼續賦值,所以最後的返回值是 2。

  1. panic

panic 是 Go 的內置函數,可以打斷當前的代碼的正常執行流程,如果一個函數中出現 panic,該函數後續的代碼都會停止執行。但是會執行 F 中的 defer 代碼。然後其他調用 F 函數的地方也會出現 panic,層層向上傳遞,直到棧頂,最後程序會崩潰。

panic 可以顯示調用 panic 函數產生,也會通過一些運行時的錯誤產生,比如數組越界。

在網上有很多文章會說,panic 只會調用當前 goroutine 的 defer 代碼,其實這種說法是不正確的,比如下面的代碼:

func main() {
  go func() {
    defer fmt.Println("goroutine1 invoke")
    go func() {
      defer fmt.Println("goroutine2 invoke")
      go func() {
        defer fmt.Println("goroutine3 invoke")
        panic("panic")
      }()
    }()
  }()
  time.Sleep(1 * time.Second)
}

很大概率會輸出下面的結果:

goroutine1 invoke
goroutine2 invoke
goroutine3 invoke
panic: panic

準確的說法是 panic 只會保證當前 goroutine 中的 defer 代碼一定會執行,其他 goroutine 中的 defer 代碼不保證能執行。

  1. recover

recover 也是 Go 的內置函數,這個函數可以從 panic 中恢復程序的正常執行。recover 需要和 defer 定義在一起。

在正常的流程中,recover 的執行不會產生任何影響。只有在 panic 發生的時候,recevoer 纔會恢復應用,阻止程序崩潰。而 panic 發生的時候只會執行 defer 代碼。所以 recover 只在和 defer 搭配的時候纔會有意義。

recover 和 panic 需要在同一個 goroutine  使用,跨 goroutine 無法恢復應用

go func() {
    defer fmt.Println("goroutine1 invoke")
    go func() {
      defer fmt.Println("goroutine2 invoke")
      go func() {
        defer func() {
          recover()
        }()
        defer fmt.Println("goroutine3 invoke")
        panic("panic")
      }()
    }()
  }()
time.Sleep(1 * time.Second)

下面的程序不會出現崩潰,但如果對 recover 的調用不在同一個 goroutine 中,就無法阻止程序的崩潰。

go func() {
    defer fmt.Println("goroutine1 invoke")
    go func() {
      defer func() {
        recover()
      }()
      defer fmt.Println("goroutine2 invoke")
      go func() {
        defer fmt.Println("goroutine3 invoke")
        panic("panic")
      }()
    }()
  }()
  time.Sleep(1 * time.Second)
  1. 小結

defer、panic、recover 是 Go 提供的流程控制方式,defer 可以用於正常的代碼流程,用於關閉資源等操作,panic 則用來表示程序出現大問題,需要終止,可以自行觸發,也可以被一些運行時的錯誤觸發。但在一些情況下,我們不希望程序因爲 panic 而終止,比如 web 服務,可以通過 recoever 來恢復程序。

本文基於 go1.14

文 / Rayjun

[1] https://blog.golang.org/defer-panic-and-recover

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P2BzlvbHtS1H_Ewz5JMZmQ