3. Go中panic與recover注意事項

1. 前言

Go 語言中兩個經常成對出現的兩個關鍵字 — panic 和 recover。這兩個關鍵字與上一節提到的 defer 有緊密的聯繫,它們都是 Go 語言中的內置函數,也提供了互補的功能。

需要說明兩點

  1. panic 能夠改變程序的控制流,調用 panic 後會立刻停止執行當前函數的剩餘代碼,並在當前 Goroutine 中遞歸執行調用方的 defer

    • 立刻停止執行當前函數的剩餘代碼
    • 當前goroutine中遞歸執行調用 defer
  2. recover 可以中止 panic 造成的程序崩潰。它是一個只能在 defer 中發揮作用的函數,在其他作用域中調用不會發揮作用;

    • recover只能與defer結合使用

2. 現象

  • panic 只會觸發當前goroutine的defer
  • revoce 只有在defer中調用才能生效
  • panic 允許在defer中嵌套多磁調用

2.1 跨協程失效

首先要介紹的現象是 panic 只會觸發當前 Goroutine 的延遲函數調用,通過如下所示的代碼瞭解該現象:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 主線程中的defer函數並不會執行,因爲子協程 panic後,主線程中的defer並不會執行
	defer println("in main")

	go func() {
		defer println("in goroutine")
		fmt.Println("子協程running")
		panic("子協程崩潰")
	}()

	time.Sleep(1 * time.Second)
}
# 輸出
$ go run main.go
子協程running
in goroutine
panic: 子協程崩潰

goroutine 6 [running]:
main.main.func1()
... 

當運行這段代碼時會發現 main 函數中的 defer 語句並沒有執行,執行的只有當前 Goroutine 中的 defer。

2.2 不起作用的recover

初學 Go 語言工程師可能會寫出下面的代碼,在主程序中調用 recover 試圖中止程序的崩潰,但是從運行的結果中也能看出,下面的程序沒有正常退出。

package main

import "fmt"

func main() {
	defer fmt.Println("in main")
	if err := recover(); err != nil {
		fmt.Println(err)
	}

	panic("unknown err")
}
# 輸出
$ go run main.go
in main
panic: unknown err

goroutine 1 [running]:
main.main()
        D:/gopath/src/Go_base/lesson/panic/demo5.go:11 +0x125

仔細分析一下這個過程就能理解這種現象背後的原因,recover 只有在發生 panic 之後調用纔會生效。然而在上面的控制流中,recover 是在 panic 之前調用的,並不滿足生效的條件,所以我們需要在 defer 中使用 recover 關鍵字。

正確的寫法應該是這樣:

package main

import "fmt"

func main() {
	defer fmt.Println("in main")
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("occur error")
			fmt.Println(err)
		}
	}()

	panic("unknown err")

}

2.3 嵌套使用panic

panic 是可以多次嵌套調用的。,如下所示的代碼就展示瞭如何在 defer 函數中多次調用 panic:

package main

import "fmt"

func main() {
	defer fmt.Println("in main")
	defer func() {
		defer func() {
			panic("panic again and again")
		}()
		panic("panic again")
	}()

	panic("panic once")
}
# 輸出
$ go run main.go
in main
panic: panic once
        panic: panic again
        panic: panic again and again

goroutine 1 [running]:
main.main.func1.1()
...

從上述程序輸出的結果,我們可以確定程序多次調用 panic 也不會影響 defer 函數的正常執行,所以使用 defer 進行收尾工作一般來說都是安全的。

3. panic數據結構

panic 關鍵字在源代碼是由數據結構 runtime._panic 表示的。每當調用panic 都會創建一個如下所示的數據結構存儲相關信息:

type _panic struct {
	argp      unsafe.Pointer
	arg       interface{}
	link      *_panic
	recovered bool
	aborted   bool
	pc        uintptr
	sp        unsafe.Pointer
	goexit    bool
}
  1. argp 是指向 defer 調用時參數的指針;
  2. arg 是調用 panic 時傳入的參數;
  3. link 指向了更早調用的 runtime._panic結構;
  4. recovered 表示當前 runtime._panic 是否被 recover 恢復;
  5. aborted 表示當前的 panic 是否被強行終止;

具體的panic 程序崩潰與恢復崩潰原理在此不做延伸, 可參考panic與recover

4. 小結

簡單總結一下程序崩潰和恢復的過程:

  1. 編譯器會負責做轉換關鍵字的工作
    • 將 panic 和 recover 分別轉換成 runtime.gopanic 和 runtime.gorecover;
    • 將 defer 轉換成 runtime.deferproc 函數
    • 在調用 defer 的函數末尾調用 runtime.deferreturn 函數;
  2. 在運行過程中遇到 runtime.gopanic 方法時,會從 Goroutine 的鏈表依次取出 runtime._defer 結構體並執行;
  3. 如果調用延遲執行函數時遇到了 runtime.gorecover 就會將 _panic.recovered 標記成 true 並返回 panic 的參數;
    • 在這次調用結束之後,runtime.gopanic 會從 runtime._defer 結構體中取出程序計數器 pc 和棧指針 sp 並調用 runtime.recovery 函數進行恢復程序;
    • runtime.recovery 會根據傳入的 pc 和 sp 跳轉回 runtime.deferproc;
    • 編譯器自動生成的代碼會發現 runtime.deferproc 的返回值不爲 0,這時會跳回 runtime.deferreturn 並恢復到正常的執行流程;
  4. 如果沒有遇到 runtime.gorecover 就會依次遍歷所有的 runtime._defer,並在最後調用 runtime.fatalpanic 中止程序、打印 panic 的參數並返回錯誤碼 2;

5. 參考

  1. https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/
  2. https://blog.golang.org/defer-panic-and-recover
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章