精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

我所理解的 Go 的 `panic` / `defer` / `recover` 異常處理機制

開發(fā) 前端
本文將深入探討 Go 語言中 panic、defer? 和 recover? 的概念、它們之間的交互流程以及一些內部實現(xiàn)相關的細節(jié)。

Go 語言中的錯誤處理方式(Error Handle)常常因其顯式的 if err != nil 判斷而受到一些討論。但這背后蘊含了 Go 的設計哲學:區(qū)別于 Java、C++ 或 Python 等語言中常見的 try/catch 或 except 傳統(tǒng)異常處理機制,Go 語言鼓勵通過函數(shù)返回 error 對象來處理可預見的、常規(guī)的錯誤。而對于那些真正意外的、無法恢復的運行時錯誤,或者嚴重的邏輯錯誤,Go 提供了 panic、defer 和 recover 這一套機制來處理。

具體而言:

  1. panic 是一個內置函數(shù),用于主動或由運行時觸發(fā)一個異常狀態(tài),表明程序遇到了無法繼續(xù)正常執(zhí)行的嚴重問題。一旦 panic 被觸發(fā),當前函數(shù)的正常執(zhí)行流程會立即停止。
  2. defer 語句用于注冊一個函數(shù)調用,這個調用會在其所在的函數(shù)執(zhí)行完畢(無論是正常返回還是發(fā)生 panic)之前被執(zhí)行。defer 調用的執(zhí)行遵循“先進后出”(LIFO, Last-In-First-Out)的原則。
  3. recover 是一個內置函數(shù),專門用于捕獲并處理 panic。重要的是,recover 只有在 defer 注冊的函數(shù)內部直接調用時才有效。

本文將深入探討 Go 語言中 panic、defer 和 recover 的概念、它們之間的交互流程以及一些內部實現(xiàn)相關的細節(jié)。希望通過本文的闡述,能夠逐漸明晰一些圍繞它們的使用“規(guī)矩”所帶來的疑惑,例如:為什么 recover 必須直接在 defer 函數(shù)中調用?defer 是如何確保其“先進后出”的執(zhí)行順序的?以及為什么在 defer 語句后常常推薦使用一個閉包(closure)?

panic 是什么

panic 是 Go 語言中的一個內置函數(shù),用于指示程序遇到了一個不可恢復的嚴重錯誤,或者說是一種運行時恐慌。當 panic 被調用時,它會立即停止當前函數(shù)的正常執(zhí)行流程。緊接著,程序會開始執(zhí)行當前 goroutine 中所有被 defer 注冊的函數(shù)。這個執(zhí)行 defer 函數(shù)的過程被稱為“恐慌過程”或“展開堆?!保╱nwinding the stack)。如果在執(zhí)行完所有 defer 函數(shù)后,該 panic 沒有被 recover 函數(shù)捕獲并處理,那么程序將會終止,并打印出 panic 的值以及相關的堆棧跟蹤信息。

panic 可以由程序主動調用,例如 panic("something went wrong"),也可以由運行時錯誤觸發(fā),比如數(shù)組越界訪問、空指針引用等。

我們來看一個簡單的例子:

package main

import"fmt"

func main() {
    fmt.Println("程序開始")
    triggerPanic()
    fmt.Println("程序結束 - 這行不會被執(zhí)行") // 因為 panic 未被恢復,程序會終止
}

func triggerPanic() {
    defer fmt.Println("defer in triggerPanic: 1") // 這個 defer 會在 panic 發(fā)生后執(zhí)行
    fmt.Println("triggerPanic 函數(shù)執(zhí)行中...")
    var nums []int
    // 嘗試訪問一個 nil 切片的元素,這將引發(fā)運行時 panic
    fmt.Println(nums[0]) // 這里會 panic
    defer fmt.Println("defer in triggerPanic: 2") // 這個 defer 不會執(zhí)行,因為它在 panic 之后
    fmt.Println("triggerPanic 函數(shù)即將結束 - 這行不會被執(zhí)行")
}
程序開始
triggerPanic 函數(shù)執(zhí)行中...
defer in triggerPanic: 1
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.triggerPanic()
        /home/piperliu/code/playground/main.go:16 +0x8f
main.main()
        /home/piperliu/code/playground/main.go:7 +0x4f
exit status 2

在上述代碼中,triggerPanic 函數(shù)中的 fmt.Println(nums[0]) 會因為對 nil 切片進行索引操作而觸發(fā)一個運行時 panic。一旦 panic 發(fā)生:

  1. triggerPanic 函數(shù)的正常執(zhí)行立即停止。
  2. 在 panic 發(fā)生點之前注冊的 defer fmt.Println("defer in triggerPanic: 1") 會被執(zhí)行。
  3. 由于 panic 沒有在 triggerPanic 或 main 中被 recover,程序會終止,并輸出 panic 信息和堆棧。
  4. 因此,main 函數(shù)中的 fmt.Println("程序結束 - 這行不會被執(zhí)行") 以及 triggerPanic 函數(shù)中 panic 點之后的代碼都不會執(zhí)行。

defer 是什么?

defer 是 Go 語言中的一個關鍵字,用于將其后的函數(shù)調用(我們稱之為延遲函數(shù)調用)推遲到包含 defer 語句的函數(shù)即將返回之前執(zhí)行。這種機制非常適合用于執(zhí)行一些清理工作,例如關閉文件、釋放鎖、記錄函數(shù)結束等。

defer 的一個重要特性是其參數(shù)的求值時機。當 defer 語句被執(zhí)行時,其后的函數(shù)調用所需的參數(shù)會 立即被求值并保存 ,但函數(shù)本身直到外層函數(shù)即將退出時才會被真正調用。這意味著,如果延遲函數(shù)調用的參數(shù)是一個變量,那么在 defer 語句執(zhí)行時該變量的值就被確定了,后續(xù)對該變量的修改不會影響到已注冊的延遲函數(shù)調用中該參數(shù)的值。

另一個關鍵特性是,如果一個函數(shù)內有多個 defer 語句,它們的執(zhí)行順序是“先進后出”(LIFO)。也就是說,最先被 defer 的函數(shù)調用最后執(zhí)行,最后被 defer 的函數(shù)調用最先執(zhí)行,就像一個棧結構。

考慮下面的代碼示例:

package main

import"fmt"

func main() {
    fmt.Println("main: 開始")
    value := 1
    defer fmt.Println("第一個 defer, value =", value) // value 的值 1 在此時被捕獲

    value = 2
    defer fmt.Println("第二個 defer, value =", value) // value 的值 2 在此時被捕獲

    value = 3
    fmt.Println("main: value 最終為", value)
    fmt.Println("main: 結束")
}
main: 開始
main: value 最終為 3
main: 結束
第二個 defer, value = 2
第一個 defer, value = 1

從輸出可以看出,defer 語句注冊的函數(shù)調用的參數(shù)是在 defer 語句執(zhí)行時就確定了的。并且,第二個 defer 語句(最后注冊的)先于第一個 defer 語句(最先注冊的)執(zhí)行,體現(xiàn)了 LIFO 的原則。

defer 語句常與匿名函數(shù)(閉包)結合使用,這可以方便地在延遲執(zhí)行的邏輯中訪問和修改其外層函數(shù)的命名返回值,或者執(zhí)行更復雜的邏輯。

recover 是什么?

recover 是 Go 語言中一個用于“恢復”程序從 panic 狀態(tài)的內置函數(shù)。當一個 goroutine 發(fā)生 panic 時,它會停止當前函數(shù)的執(zhí)行,并開始執(zhí)行所有已注冊的 defer 函數(shù)。如果在這些 defer 函數(shù)中,有一個直接調用了 recover(),并且這個 recover() 調用捕獲到了一個 panic(即 recover() 的返回值不為 nil),那么這個 panic 過程就會停止。

recover 的核心規(guī)則和調用時機非常關鍵:

  1. recover 必須在 defer 函數(shù)中直接調用才有效。 如果在 defer 調用的函數(shù)中再嵌套一層函數(shù)去調用 recover,那是無法捕獲 panic 的。
  2. 如果當前 goroutine 沒有發(fā)生 panic,或者 recover 不是在 defer 函數(shù)中調用的,那么 recover() 會返回 nil,并且沒有任何其他效果。
  3. 如果 recover() 成功捕獲了一個 panic,它會返回傳遞給 panic 函數(shù)的參數(shù)。此時,程序的執(zhí)行會從調用 defer 的地方恢復,恢復后函數(shù)就準備返回了。原先的 panic 過程則被終止,程序不會崩潰。

可以認為,recover 給予了程序一個在發(fā)生災難性錯誤時進行“自救”的機會。它允許程序捕獲 panic,記錄錯誤信息,執(zhí)行一些清理操作,然后可能以一種比直接崩潰更優(yōu)雅的方式繼續(xù)執(zhí)行或終止。

一個典型的使用 recover 的模式如下:

package main

import"fmt"

func main() {
    fmt.Println("主函數(shù)開始")
    safeDivide(10, 0)
    safeDivide(10, 2)
    fmt.Println("主函數(shù)結束")
}

func safeDivide(a, b int) {
    deferfunc() {
        // 這個匿名函數(shù)是一個 defer 函數(shù)
        if r := recover(); r != nil {
            // r 是 panic 傳遞過來的值
            fmt.Printf("捕獲到 panic: %v\n", r)
            fmt.Println("程序已從 panic 中恢復,繼續(xù)執(zhí)行...")
        }
    }() // 注意這里的 (),表示定義并立即調用該匿名函數(shù)(實際上是注冊)

    fmt.Printf("嘗試 %d / %d\n", a, b)
    if b == 0 {
        panic("除數(shù)為零!") // 主動 panic
    }
    result := a / b
    fmt.Printf("結果: %d\n", result)
}
主函數(shù)開始
嘗試 10 / 0
捕獲到 panic: 除數(shù)為零!
程序已從 panic 中恢復,繼續(xù)執(zhí)行...
嘗試 10 / 2
結果: 5
主函數(shù)結束

在這個例子中,當 safeDivide(10, 0) 被調用時,會觸發(fā) panic("除數(shù)為零!")。此時,defer 注冊的匿名函數(shù)會被執(zhí)行。在該匿名函數(shù)內部,recover() 捕獲到這個 panic,打印信息,然后 safeDivide 函數(shù)結束。程序會繼續(xù)執(zhí)行 main 函數(shù)中的下一條語句 safeDivide(10, 2),而不會因為第一次除零錯誤而崩潰。

panic/defer/recover 的交互流程

為了更清晰地理解 panic、defer 和 recover 之間的協(xié)同工作方式,我們通過一個稍微復雜一點的例子來追蹤程序的執(zhí)行流程。

假設我們有如下函數(shù) A、B、C 和 main:

package main

import"fmt"

func C(level int) {
    fmt.Printf("進入 C (層級 %d)\n", level)
    defer fmt.Printf("defer in C (層級 %d)\n", level)

    if level == 1 {
        panic(fmt.Sprintf("在 C (層級 %d) 中發(fā)生 panic", level))
    }
    fmt.Printf("離開 C (層級 %d)\n", level)
}

func B() {
    fmt.Println("進入 B")
    deferfunc() {
        fmt.Println("defer in B (開始)")
        if r := recover(); r != nil {
            fmt.Printf("在 B 中恢復: %v\n", r)
        }
        fmt.Println("defer in B (結束)")
    }()

    C(1) // 調用 C,這將觸發(fā) panic
    fmt.Println("離開 B - 即便 C 中的 panic 被恢復,這里也不會執(zhí)行,因為 defer 在之后調用")
}

func A() {
    fmt.Println("進入 A")
    defer fmt.Println("defer in A")
    C(2) // 調用 C,這次不會 panic
    fmt.Println("離開 A")
}

func main() {
    fmt.Println("main: 開始")
    A()
    fmt.Println("=== 分割線 ===")
    B()
    fmt.Println("main: 結束")
}
main: 開始
進入 A
進入 C (層級 2)
離開 C (層級 2)
defer in C (層級 2)
離開 A
defer in A
=== 分割線 ===
進入 B
進入 C (層級 1)
defer in C (層級 1)
defer in B (開始)
在 B 中恢復: 在 C (層級 1) 中發(fā)生 panic
defer in B (結束)
main: 結束

實現(xiàn)原理與數(shù)據(jù)結構

要理解 panic/defer/recover 的工作機制,我們需要了解一些 Go 運行時內部與之相關的數(shù)據(jù)結構。這些細節(jié)通常對日常編程是透明的,但有助于深入理解其行為。

關鍵的數(shù)據(jù)結構主要與 goroutine(g)本身,以及 _defer 和 _panic 記錄相關聯(lián)。

g (Goroutine)

每個 goroutine 在運行時都有一個對應的 g 結構體(在 runtime/runtime2.go 中定義)。這個結構體包含了 goroutine 的所有狀態(tài)信息,包括其棧指針、調度狀態(tài)等。與我們討論的主題密切相關的是,g 結構體中通常會包含指向 _defer 記錄鏈表頭和 _panic 記錄鏈表頭的指針。

  • _defer:一個指向 _defer 記錄鏈表頭部的指針。每當執(zhí)行一個 defer 語句,一個新的 _defer 記錄就會被創(chuàng)建并添加到這個鏈表的頭部。
  • _panic:一個指向 _panic 記錄鏈表頭部的指針。當 panic 發(fā)生時,一個 _panic 記錄被創(chuàng)建并鏈接到這里。

_defer 結構體

每當一個 defer 語句被執(zhí)行,運行時系統(tǒng)會創(chuàng)建一個 _defer 結構體實例。這個結構體大致包含以下信息:

  • siz:參數(shù)和結果的總大小。
  • fn:一個指向被延遲調用的函數(shù)(的函數(shù)值 funcval)的指針。
  • sp:延遲調用發(fā)生時的棧指針。
  • pc:延遲調用發(fā)生時的程序計數(shù)器。
  • link:指向前一個(即下一個要執(zhí)行的)_defer 記錄的指針,形成一個單向鏈表。新的 _defer 總是被添加到鏈表的頭部,所以這個鏈表天然地實現(xiàn)了 LIFO 的順序。
  • 參數(shù)區(qū)域:緊隨 _defer 結構體的是實際傳遞給延遲函數(shù)的參數(shù)值。這些參數(shù)在 defer 語句執(zhí)行時就被復制并存儲在這里。

_panic 結構體

當 panic 發(fā)生時,運行時會創(chuàng)建一個 _panic 結構體。它通常包含:

  • argp:指向 panic 參數(shù)的接口值的指針(已廢棄,現(xiàn)在通常用 arg)。
  • arg:傳遞給 panic 函數(shù)的參數(shù)(通常是一個 interface{})。
  • link:指向上一個(外層的)_panic 記錄。這用于處理嵌套 panic 的情況(例如,一個 defer 函數(shù)本身也 panic 了)。
  • recovered:一個布爾標記,指示這個 panic 是否已經被 recover 處理。
  • aborted:一個布爾標記,指示這個 panic 是否是因為調用了 runtime.Goexit() 而非真正的 panic。

這些結構體在 Go 語言的 runtime 包中定義,它們是實現(xiàn) panic/defer/recover 機制的基石。通過在 g 中維護 _defer 和 _panic 的鏈表,Go 運行時能夠在 panic 發(fā)生時正確地展開堆棧、執(zhí)行延遲函數(shù),并允許 recover 來捕獲和處理這些 panic。

_defer 的入棧與調用流程

值得注意的是,我們應該首先理解 return xxx 語句。實際上,這個語句會被編譯器拆分為三條指令:

  1. 返回值 = xxx
  2. 調用 defer 函數(shù)
  3. 空的 return

當程序執(zhí)行到一個 defer 語句時,Go 運行時會執(zhí)行 runtime.deferproc 函數(shù)(或類似功能的內部函數(shù))。這個過程大致如下:

  1. 分配 _defer 記錄 :運行時會分配一個新的 _defer 結構體。這個結構體的大小不僅包括 _defer 本身的字段,還包括了為延遲函數(shù)的參數(shù)所預留的空間。
  2. 參數(shù)立即求值與復制 :defer 語句后面跟著的函數(shù)調用的參數(shù),會在此時被立即計算出來,并將其值復制到新分配的 _defer 記錄的參數(shù)區(qū)域。這就是為什么 defer 函數(shù)能“記住”注冊它時參數(shù)的值,即使這些參數(shù)在后續(xù)代碼中被修改。
  3. 保存上下文信息 :_defer 記錄中會保存延遲調用的函數(shù)指針 (fn),以及當前的程序計數(shù)器 (pc) 和棧指針 (sp)。
  4. 鏈接到 g 的 _defer 鏈表 :新的 _defer 記錄會被添加到當前 goroutine (g) 的 _defer 鏈表的頭部。g.defer 指針會更新為指向這個新的 _defer 記錄,而新的 _defer 記錄的 link 字段會指向原先的鏈表頭(即上一個 _defer 記錄)。由于總是從頭部插入,這自然形成了“先進后出”(LIFO)的結構。

調用流程(函數(shù)返回或 panic 時)

當包含 defer 語句的函數(shù)即將返回(無論是正常返回還是因為 panic)時,運行時會檢查當前 goroutine 的 _defer 鏈表。這個過程由 runtime.deferreturn(或類似函數(shù))處理:

  1. 從 g 的 _defer 鏈表頭部取出一個 _defer 記錄。
  2. 如果鏈表為空,則沒有 defer 函數(shù)需要執(zhí)行。
  3. 如果取出的 _defer 記錄有效:

將其從鏈表中移除(即將 g.defer 指向該記錄的 link)。

將保存在 _defer 記錄中的參數(shù)復制到當前棧幀,為調用做準備。

調用 _defer 記錄中保存的函數(shù)指針 fn。

延遲函數(shù)執(zhí)行完畢后,重復此過程,直到 _defer 鏈表為空。

立即求值參數(shù)是什么?

正如前面強調的,defer 關鍵字后的函數(shù)調用,其參數(shù)的值是在 defer 語句執(zhí)行的時刻就被計算并存儲起來的,而不是等到外層函數(shù)結束、延遲函數(shù)真正被調用時才計算。

  • 為什么推薦在 defer 后接一個閉包?
  • 訪問外層函數(shù)作用域 :閉包可以捕獲其定義時所在作用域的變量。這使得 defer 的邏輯可以方便地與外層函數(shù)的狀態(tài)交互,例如修改命名返回值,或者訪問在 defer 語句時尚未聲明但在函數(shù)返回前會賦值的變量。
  • 執(zhí)行復雜邏輯 :如果 defer 需要執(zhí)行的不僅僅是一個簡單的函數(shù)調用,而是一系列操作,閉包提供了一種簡潔的方式來封裝這些操作。
  • 正確處理循環(huán)變量 :在循環(huán)中使用 defer 時,如果不使用閉包并把循環(huán)變量作為參數(shù)傳遞給閉包,那么所有 defer 語句將共享同一個循環(huán)變量的最終值。通過閉包并傳遞參數(shù),可以捕獲每次迭代時循環(huán)變量的當前值。
package main

import"fmt"

type Test struct {
    Name string
}

func (t Test) hello() {
    fmt.Printf("Hello, %s\n", t.Name)
}

func (t *Test) hello2() {
    fmt.Printf("pointer: %s\n", t.Name)
}

func runT(t Test) {
    t.hello()
}

func main() {
    mapt := []Test{
        {Name: "A"},
        {Name: "B"},
        {Name: "C"},
    }

    for _, t := range mapt {
        defer t.hello()
        defer t.hello2()
    }
}

輸出如下:

piperliu@go-x86:~/code/playground$ gvm use go1.22.0
Now using version go1.22.0
piperliu@go-x86:~/code/playground$ go run main.go 
pointer: C
Hello, C
pointer: B
Hello, B
pointer: A
Hello, A
piperliu@go-x86:~/code/playground$ gvm use go1.21.0
Now using version go1.21.0
piperliu@go-x86:~/code/playground$ go run main.go 
pointer: C
Hello, C
pointer: C
Hello, B
pointer: C
Hello, A

你可以看到 go1.21.0 和 go1.22.0 的表現(xiàn)是不同的。在這個例子中,我們把兩次 defer 放到了 for 循環(huán)里,分別調用了接收者為值的方法 hello 和接收者為指針的方法 hello2。按 Go 的規(guī)范,每一個 defer 語句都會生成一個“閉包”(closure),而這個閉包會 捕獲(capture) 循環(huán)變量 t。下面分兩部分來詳細說明其行為差異:

值接收者(func (t Test) hello())的 defer

  • 當你寫下 defer t.hello() 時,編譯器會把這一調用包裝成一個閉包,并且在閉包內部保存一份 拷貝 (copy)——也就是當時 t 的值。
  • 因此,不管后續(xù)循環(huán)中 t 如何變化,已經創(chuàng)建好的這些閉包都各自持有自己那一刻的獨立拷貝。等待 main 函數(shù)退出時,它們會按 LIFO(后進先出)的順序依次執(zhí)行,每個閉包都打印自己持有的那個副本的 Name 字段,結果正好是 C、B、A。

指針接收者(func (t *Test) hello2())的 defer

  • 寫成 defer t.hello2() 時,閉包并不拷貝 Test 結構本身,而是拷貝了一個 指向循環(huán)變量 t 的指針 。
  • 關鍵在于:在 Go 1.21 之前,循環(huán)變量 t 本身在每次迭代中都是 同一個變量 (地址不變),只是不斷被重寫(rewritten)成新的值。這樣,所有那些指針閉包實際上都指向同一個內存地址——最后一次迭代結束時,這個地址中存放的是 {Name: "C"}。
  • 因此,當程序末尾逐個執(zhí)行這些 defer 時,hello2 全部都訪問的正是指向同一個變量的指針,輸出的名字也就全是最后一次給 t 賦的 "C"。

Go 1.22 中的變化

  • 從 Go 1.22 起,規(guī)范做了一個重要的調整:* 循環(huán)頭部的迭代變量在每一輪都會被當作“全新”的變量來處理* ,也就是說每次迭代編譯器都會隱式地為 t 重新聲明一次、分配一次新的內存地址。
  • 這樣一來,即便是拿指針去捕獲,每次也捕獲的是 不同 的變量地址,閉包就能各自綁定當時那一輪迭代的 t,輸出也就跟值接收者那邊一樣,依次是 C、B、A。

總結:

  • 值接收者 的 defer 總是捕獲當時的值拷貝,跟循環(huán)變量的重寫行為無關;
  • 指針接收者 的 defer 捕獲的是循環(huán)變量的地址,若循環(huán)變量重用同一地址(如 Go 1.21 及以前版本),所有閉包共用最終那次迭代的內容;
  • Go 1.22 以后 ,循環(huán)變量地址不再重用,從而讓指針閉包也能如值閉包般,捕獲每一輪獨立的變量,實現(xiàn)與 Go 1.21+ 值接收者一致的行為。

(上面這個例子搬運自 StackOverflow: Golang defers in a for loop behaves differently for the same struct - https://stackoverflow.com/a/75908307/11564718 )

_panic 的傳播流程與內部細節(jié)

當程序執(zhí)行 panic(v) 或者發(fā)生運行時錯誤(如空指針解引用、數(shù)組越界)時,Go 運行時會調用 runtime.gopanic(interface{}) 函數(shù)。這個函數(shù)是 panic 機制的核心。

其大致流程如下:

  1. 創(chuàng)建 _panic 記錄
  • 運行時系統(tǒng)首先創(chuàng)建一個 _panic 結構體實例。
  • 這個結構體的 arg 字段會被設置為傳遞給 panic 的值 v。
  • link 字段會指向當前 goroutine (g) 可能已經存在的 _panic 記錄(g._panic)。這種情況發(fā)生在 defer 函數(shù)執(zhí)行過程中又觸發(fā)了新的 panic(嵌套 panic)。新 panic 會覆蓋舊 panic,舊的 panic 信息會通過 link 鏈起來。
  • recovered 字段初始化為 false。
  • 新創(chuàng)建的 _panic 記錄會被設置為當前 goroutine 的活動 panic,即 g._panic 指向這個新記錄。
  1. 開始棧展開(Stack Unwinding)與執(zhí)行 defer
  • 對應的延遲函數(shù)被調用。
  • 關鍵點 :如果在這個延遲函數(shù)內部直接調用了 recover(),并且 recover() 成功捕獲了當前的 panic(即 g._panic 所指向的 panic),那么 g._panic.recovered 標記會被設為 true。gopanic 函數(shù)會注意到這個標記,停止繼續(xù)展開 _defer 鏈,并開始執(zhí)行恢復流程(見下一節(jié) recover 的實現(xiàn))。
  • 如果延遲函數(shù)執(zhí)行完畢后,panic 沒有被 recover,或者延遲函數(shù)本身又觸發(fā)了新的 panic,gopanic 會繼續(xù)處理(新的 panic 會取代當前的,然后繼續(xù)執(zhí)行 defer 鏈)。
  • 如果延遲函數(shù)正常執(zhí)行完畢且未 recover,則繼續(xù)循環(huán),處理下一個 _defer。
  • gopanic 進入一個循環(huán),不斷地從當前 goroutine 的 _defer 鏈表頭部取出 _defer 記錄并執(zhí)行它們。
  • 對于每一個取出的 _defer:
  1. defer 鏈執(zhí)行完畢后
  • 如果在所有 defer 函數(shù)執(zhí)行完畢后,g._panic.recovered 仍然是 false(即 panic 沒有被任何 recover 調用捕獲),那么 gopanic 會調用 runtime.fatalpanic。
  • runtime.fatalpanic 會打印出當前的 panic 值 (g._panic.arg) 和發(fā)生 panic 時的調用堆棧信息。
  • 最后,程序會以非零狀態(tài)碼退出,通常是2。

匯編層面與棧展開的理解

雖然我們通常不直接接觸匯編,但理解其概念有助于明白“棧展開”。當一個函數(shù)調用另一個函數(shù)時,返回地址、參數(shù)、局部變量等會被壓入當前 goroutine 的棧。發(fā)生 panic 時,gopanic 的過程實際上就是在模擬函數(shù)返回的過程,但它不是正常返回,而是逐個“彈出”棧幀(邏輯上),并查找與這些棧幀關聯(lián)的 _defer 記錄來執(zhí)行。如果 panic 未被 recover,這個展開過程會一直持續(xù)到 goroutine 棧的最初始調用者,最終導致程序終止。這個過程由運行時系統(tǒng)精心管理,確保 defer 的正確執(zhí)行和 recover 的有效性。

總的來說,_panic 的傳播是一個受控的?;厮葸^程,它給予了 defer 函數(shù)介入并可能通過 recover 來中止這一傳播的機會。

recover 的實現(xiàn)

recover 的實現(xiàn)與 panic 的流程緊密相連,它在 runtime.gorecover(argp unsafe.Pointer) interface{} 函數(shù)中實現(xiàn)。

recover 的執(zhí)行流程:

  1. 檢查調用上下文 :gorecover 首先會檢查它是否在正確的上下文中被調用。最關鍵的檢查是當前 goroutine (g) 是否正處于 panic 狀態(tài)(即 g._panic != nil)并且這個 panic 尚未被標記為 recovered(g._panic.recovered == false)。
  • 如果 g._panic 為 nil(沒有活動的 panic),或者 g._panic.recovered 為 true(panic 已經被其他 recover 調用處理過了),那么 gorecover 直接返回 nil。這解釋了為什么在沒有 panic 的情況下調用 recover 會返回 nil。
  1. 檢查是否直接在 defer 函數(shù)中調用 :Go 運行時還需要確保 recover 是被 defer 調用的函數(shù)直接調用的,而不是在 defer 函數(shù)調用的更深層函數(shù)中調用。這是通過比較調用 gorecover 時的棧指針 (argp,它指向 recover 函數(shù)的參數(shù)在棧上的位置) 與 g._defer 鏈表頭記錄的棧指針 (d.sp) 是否匹配。
  • 如果棧指針不匹配,意味著 recover 不是在最頂層的 defer 函數(shù)(即當前正在執(zhí)行的 defer)中直接調用的,這種情況下 gorecover 也會返回 nil。這就是“recover 必須直接在 defer 函數(shù)中調用”規(guī)則的由來。
  1. 標記 panic 為已恢復 :如果上述檢查都通過,說明 recover 是在合法的時機和位置被調用的:
  • gorecover 會將當前活動的 panic(即 g._panic)的 recovered 字段標記為 true。
  • 它會保存 panic 的參數(shù)值 (g._panic.arg)。
  1. 清除當前 panic :為了防止后續(xù)的 defer 或同一個 defer 中的其他 recover 再次處理同一個 panic,gorecover 會將 g._panic 設置為 nil(或者在有嵌套 panic 的情況下,將其設置為 g._panic.link,即恢復到上一個 panic 的狀態(tài))。實際上,在 gopanic 的循環(huán)中,當它檢測到 recovered 標志被設為 true 后,它會負責清理 g._panic 并調整控制流以正常返回。
  2. 返回 panic 的參數(shù) :最后,gorecover 返回之前保存的 panic 參數(shù)值。調用者(即 defer 函數(shù)中的代碼)可以通過檢查這個返回值是否為 nil 來判斷是否成功捕獲了 panic。

為什么 recover 要放在 defer 中?

從上述流程可以看出,panic 發(fā)生時,正常的代碼執(zhí)行路徑已經中斷。唯一還會被執(zhí)行的代碼就是 defer 鏈中的函數(shù)。因此,recover 只有在 defer 函數(shù)中才有機會被執(zhí)行并接觸到 panic 的狀態(tài)。運行時通過 g._panic 和 g._defer 來協(xié)調這一過程,recover 正是這個協(xié)調機制中的一個鉤子,允許 defer 函數(shù)介入 panic 的傳播。

嵌套 panic 的情況

如果一個 defer 函數(shù)在執(zhí)行過程中自己也調用了 panic(我們稱之為 panic2,而原始的 panic 為 panic1):

  1. panic2 會創(chuàng)建一個新的 _panic 記錄,這個新記錄的 link 字段會指向 panic1 對應的 _panic 記錄。
  2. g._panic 會更新為指向 panic2 的記錄。
  3. 此時,如果后續(xù)的 defer 函數(shù)(或者同一個 defer 函數(shù)中位于新 panic 之后的 recover)調用 recover,它捕獲到的是 panic2。
  4. 如果 panic2 被成功 recover,那么 g._panic 會恢復為指向 panic1 的記錄(通過 link)。程序會繼續(xù)執(zhí)行 defer 鏈,此時 panic1 仍然是活動的,除非它也被后續(xù)的 recover 處理。
  5. 如果 panic2 沒有被 recover,那么 panic2 會取代 panic1 成為最終導致程序終止的 panic。

這種設計確保了最近發(fā)生的 panic 優(yōu)先被處理。

總結

panic、defer 和 recover 共同構成了 Go 語言中處理嚴重錯誤和執(zhí)行資源清理的補充機制。

defer 對性能的影響與技術取舍

defer 并非沒有成本。每次 defer 調用都會涉及到 runtime.deferproc 的執(zhí)行,包括分配 _defer 對象、復制參數(shù)等操作。在函數(shù)返回時,還需要 runtime.deferreturn 來遍歷 _defer 鏈并執(zhí)行延遲調用。相比于直接的函數(shù)調用,這無疑會帶來一些額外的開銷。在性能極其敏感的內層循環(huán)中,大量使用 defer 可能會成為瓶頸。

然而,這種開銷在大多數(shù)情況下是可以接受的,尤其是考慮到 defer 帶來的代碼清晰度和健壯性提升。它確保了資源(如文件句柄、網絡連接、鎖等)即使在函數(shù)發(fā)生 panic 或有多個返回路徑時也能被正確釋放,極大地減少了資源泄漏的風險。這是一種典型的在輕微性能開銷與代碼可維護性、可靠性之間的權衡。Go 的設計者認為這種權衡是值得的。

設計哲學

Go 語言的設計哲學強調顯式和清晰。對于可預期的錯誤(如文件不存在、網絡超時等),Go 推薦使用多返回值,將 error 作為最后一個返回值來顯式地處理。這種方式使得錯誤處理成為代碼流程中正常的一部分,而不是通過異常拋出來打斷流程。

panic 和 recover 則被保留用于處理那些真正意外的、程序無法或不應該繼續(xù)正常運行的情況,例如嚴重的運行時錯誤(空指針解引用、數(shù)組越界,盡管很多這類情況運行時會自動 panic)、或者庫代碼中不希望將內部嚴重錯誤以 error 形式暴露給調用者而直接中斷操作的情況。recover 的存在是為了給程序一個從災難性 panic 中“優(yōu)雅”恢復的機會,例如記錄日志、關閉服務,而不是粗暴地崩潰,特別是在服務器應用中,一個 goroutine 的 panic 不應該導致整個服務停止。

panic / recover 使用場景

  • 不應濫用 panic :不要用 panic 來進行普通的錯誤處理或控制程序流程。如果一個錯誤是可預期的,應該返回 error。
  • panic 的合理場景 :

a.發(fā)生真正不可恢復的錯誤,程序無法繼續(xù)執(zhí)行。例如,程序啟動時關鍵配置加載失敗。

b.檢測到程序內部邏輯上不可能發(fā)生的“不可能”狀態(tài),這通常指示一個 bug。

  • recover 的合理場景 :

a.頂層 panic 捕獲:在 main 函數(shù)啟動的 goroutine 或 Web 服務器處理每個請求的 goroutine 的頂層,設置一個 defer 和 recover 來捕獲任何未處理的 panic,記錄錯誤日志,并可能向客戶端返回一個通用錯誤響應,以防止單個請求的失敗導致整個服務崩潰。

b.庫代碼健壯性:當編寫供他人使用的庫時,如果內部發(fā)生了某種不應由調用者處理的 panic,庫自身可以在其公共 API 的邊界處使用 recover 將 panic 轉換為 error 返回,避免將內部的 panic 泄露給庫的使用者。

總而言之,defer 是一個強大的工具,用于確保清理邏輯的執(zhí)行。panic 和 recover 則提供了一種處理程序級別嚴重錯誤的機制,但應謹慎使用,以符合 Go 語言的錯誤處理哲學。

責任編輯:武曉燕 來源: Piper蛋窩
相關推薦

2025-06-03 02:00:00

2013-06-25 09:52:32

GoGo語言Go編程

2015-12-28 11:25:51

C++異常處理機制

2011-03-17 09:20:05

異常處理機制

2025-05-26 00:05:00

2024-07-26 08:32:44

panic?Go語言

2024-03-04 10:00:35

數(shù)據(jù)庫處理機制

2011-04-06 10:27:46

Java異常處理

2025-05-28 03:00:00

2011-07-21 15:20:41

java異常處理機制

2023-10-09 07:14:42

panicGo語言

2025-05-22 09:01:28

2023-03-08 08:54:59

SpringMVCJava

2025-03-31 08:57:25

Go程序性能

2021-07-03 17:53:52

Java異常處理機制

2009-08-05 18:09:17

C#異常處理機制

2010-03-05 15:40:16

Python異常

2021-03-02 09:12:25

Java異常機制

2024-02-27 10:48:16

C++代碼開發(fā)

2023-06-15 14:09:00

解析器Servlet容器
點贊
收藏

51CTO技術棧公眾號

欧美黑人xx片| 日本妇乱大交xxxxx| 欧美aaaaaaaa牛牛影院| 亚州成人在线电影| 久久久久一区二区| 中国黄色一级视频| 自拍偷拍欧美专区| 日韩大片免费观看视频播放| 欧美 国产 小说 另类| h视频在线播放| 国产精品一区二区在线播放| 97视频免费观看| 调教驯服丰满美艳麻麻在线视频 | 外国成人激情视频| 亚洲国产成人久久| 在线观看的毛片| 成人黄视频在线观看| 99久久久无码国产精品| 国产精品久久久久久久9999| 国产精品老熟女一区二区| 色爱av综合网| 日韩视频一区二区三区在线播放| 国产h视频在线播放| 日本中文字幕在线看| 成人avav在线| 91传媒视频免费| 亚洲高清在线看| 亚洲国产黄色| 久久精品国产欧美亚洲人人爽| 国产麻豆xxxvideo实拍| 成人乱码手机视频| 色婷婷国产精品综合在线观看| 国产人妻人伦精品| 成人精品一区二区三区免费| 成人激情视频网站| 91精品免费久久久久久久久| 国产suv精品一区二区33| 午夜久久美女| www.亚洲天堂| www亚洲色图| 日韩欧美黄色| 亚洲国产精品99久久| 国产黑丝在线视频| 玖玖精品在线| 欧洲国内综合视频| 日韩在线视频在线观看| 青青草原国产在线| 亚洲精品国产精品乱码不99| 亚洲日本一区二区三区在线不卡| 婷婷婷国产在线视频| 国产成人精品影院| 91超碰在线电影| 国产女18毛片多18精品| 久久成人18免费观看| 国产福利精品在线| www.久久精品视频| 蜜桃伊人久久| 浅井舞香一区二区| 国产精品美女久久久久av爽| 亚洲国产婷婷| 午夜精品蜜臀一区二区三区免费 | 午夜久久免费观看| 中文字幕久精品免费视频| 久久亚洲无码视频| 精品国产美女| 色妞色视频一区二区三区四区| 黄色av免费播放| 久久中文视频| 美女性感视频久久久| 免费中文字幕在线| 一区在线视频观看| 91av在线免费观看视频| 一本一道无码中文字幕精品热| 久久动漫亚洲| 国产精品高潮粉嫩av| 国产成人精品一区二区色戒| 久草精品在线观看| 91视频在线免费观看| 狠狠人妻久久久久久综合麻豆| 成人美女在线观看| 久久亚洲免费| 中文字幕日本在线| 亚洲精品亚洲人成人网 | 亚洲av人人澡人人爽人人夜夜| 成人做爰视频www| 欧美性感一区二区三区| 国产一级不卡毛片| 国产麻豆一区| 欧美艳星brazzers| 国产第一页视频| av资源网在线播放| 亚洲国产精品一区二区久久| 国产中文字幕视频在线观看| 国产免费不卡| 欧美性三三影院| 天堂av8在线| 4438全国亚洲精品观看视频| 亚洲成人xxx| 91精彩刺激对白露脸偷拍| 成人久久一区| 久久综合电影一区| 日本a在线观看| 免费久久99精品国产自在现线| 国产精品91在线| 一区二区三区黄| 精品午夜一区二区三区在线观看| 99在线观看视频| 午夜视频福利在线观看| 欧美国产欧美综合| 97在线国产视频| 成人国产精品入口免费视频| 欧美精品久久99久久在免费线| 丰满少妇一区二区三区专区| 天堂网av成人| 自拍偷拍免费精品| 青青草偷拍视频| 久久国产欧美| 91色精品视频在线| 欧美自拍第一页| 国产精品青草综合久久久久99| 国产在线无码精品| 欧美粗大gay| 91精品在线麻豆| 手机在线成人av| 欧美第一精品| 欧美性在线观看| 日本黄色一级视频| eeuss鲁片一区二区三区在线观看| 日韩欧美亚洲区| 免费在线看污片| 欧美视频三区在线播放| 99国产精品免费视频| 亚洲永久精品唐人导航网址| 精品激情国产视频| 亚洲欧美综合自拍| 国产成人av一区| 吴梦梦av在线| 蜜桃成人精品| 亚洲电影中文字幕| 国产性生活大片| 日韩精品电影一区亚洲| 国产激情一区二区三区在线观看 | 精品国产视频在线观看| 老司机一区二区三区| 91沈先生播放一区二区| 1pondo在线播放免费| 色哟哟在线观看一区二区三区| 亚洲一区二区图片| 日韩一区电影| 日本人成精品视频在线| 嫩草影院一区二区| 亚洲欧美日韩中文播放| 蜜臀视频一区二区三区| 麻豆国产欧美一区二区三区r| 色综合久久久久久中文网| 在线观看毛片av| 久久精品视频一区| 欧美爱爱视频免费看| 一区视频网站| 久久精品中文字幕电影| 最近中文字幕在线视频| 26uuu亚洲综合色| 国产精品三级一区二区| 日韩精品一级| 久热国产精品视频| 亚洲一区二区人妻| 国产欧美精品在线观看| av免费观看网| 亚洲三级性片| 欧美精品久久一区二区| 国产肥老妇视频| 亚洲精品少妇30p| 欧美精品色视频| 在线成人激情| 91在线短视频| 国产盗摄一区二区| 精品国产乱码久久久久久久 | 亚洲柠檬福利资源导航| jizz18女人| 日产午夜精品一线二线三线| 国产精品久久久久久久久久免费| 成人高清免费观看mv| 姬川优奈aav一区二区| 精品人妻一区二区三区蜜桃视频| 亚洲在线日韩| 欧美一区二区三区在线播放| 欧美特黄aaaaaaaa大片| 国产亚洲一级高清| 精品乱码一区内射人妻无码| 国产精品美女一区二区| 一级特黄性色生活片| 综合天堂av久久久久久久| 99re视频在线| 狼人综合视频| 一区二区三区美女xx视频| 中文字幕一区二区免费| 亚洲欧美一区二区在线观看| 中文字幕55页| 欧美成人69av| 久久青青草综合| av亚洲一区| 欧美美女15p| 午夜视频在线免费播放| 欧美亚洲愉拍一区二区| 欧美性生交大片| 国内外成人在线| 波多野结衣50连登视频| 成人影视亚洲图片在线| 亚洲影院色无极综合| 女囚岛在线观看| 国产亚洲成av人片在线观看桃| 亚洲熟女乱色一区二区三区久久久| 亚洲丝袜另类动漫二区| 日韩精品在线播放视频| 老司机午夜精品视频在线观看| 亚洲 欧洲 日韩| 欧美久久精品| 国产欧美精品va在线观看| 青草在线视频| 中文字幕亚洲一区二区三区五十路 | 国严精品久久久久久亚洲影视| 日本.亚洲电影| 欧美激情a∨在线视频播放| 免费人成在线观看网站| 欧美一区二区视频在线观看2022 | 亚洲第一综合色| 亚洲毛片亚洲毛片亚洲毛片| 成人亚洲精品久久久久软件| 91蝌蚪视频在线观看| 午夜视频精品| 日日噜噜噜噜夜夜爽亚洲精品| 91亚洲精品视频在线观看| 日本一区二区三区在线播放| 好吊日视频在线观看| 日韩精品中文字幕久久臀| 国产黄色片av| 欧美日韩亚洲综合一区| 天堂网av手机版| 一区二区三区在线视频免费观看| 免费在线观看污| 成人免费观看视频| 日本r级电影在线观看| 青娱乐精品视频在线| 欧美成人高潮一二区在线看| 91精品一区二区三区综合| 日韩激情视频| 蜜臀av免费一区二区三区| 超碰97在线播放| 亚洲午夜精品| 亚洲在线免费视频| 日本一区二区三区中文字幕 | 草美女在线观看| 精品国内亚洲在观看18黄| 国产区视频在线播放| 日韩av在线不卡| 嫩草影视亚洲| 亚洲一二三四在线观看| 国产毛片欧美毛片久久久| 成人免费电影视频| 永久免费黄色片| 久久爱www久久做| 激情内射人妻1区2区3区 | 日韩二区在线观看| 色综合久久久久无码专区| 在线观看视频免费一区二区三区| 99re6这里有精品热视频| 日本一区二区三区视频| 日韩精品电影网站| 成人av地址| 久久久免费看| 亚洲另类春色校园小说| 久久婷婷国产综合尤物精品| 欧美精品中文字幕亚洲专区| 国产欧美一区二区视频| 国产精伦一区二区三区| 国产精品亚洲不卡a| 日韩丝袜视频| 美日韩精品免费| 亚洲理论电影| 亚洲精品高清国产一线久久| 青青草91久久久久久久久| 五月天婷亚洲天综合网鲁鲁鲁| 精品国产日韩欧美| 视频一区视频二区视频| 成人羞羞网站| 一级做a爰片久久| 久久久久久久久久久妇女| 在线观看日韩片| 亚洲精品99| av无码久久久久久不卡网站| 久久一二三区| 在线观看国产中文字幕| 国产麻豆精品视频| 国产国语老龄妇女a片| 成人精品一区二区三区中文字幕| 国产 中文 字幕 日韩 在线| 久久精品亚洲精品国产欧美kt∨ | av久久网站| 成人国产精品久久久久久亚洲| 99久久婷婷国产综合精品青牛牛| 成人黄色片视频网站| 日韩福利视频一区| 日韩精品久久一区| 亚洲影视一区二区三区| 黄色激情在线视频| 性欧美暴力猛交另类hd| 日本a√在线观看| 麻豆91在线播放| 日韩少妇一区二区| 国产精品麻豆久久久| 91视频免费在线看| 欧美午夜电影在线| 国产精品老熟女视频一区二区| 亚洲成人久久一区| 色资源在线观看| 九九视频这里只有精品| 欧洲亚洲两性| 18成人免费观看网站下载| 青青草原在线亚洲| 一区视频二区视频| 影院欧美亚洲| 深夜福利网站在线观看| 91免费国产在线观看| 欧美做爰啪啪xxxⅹ性| 黑人巨大精品欧美一区二区| 中文字字幕在线中文乱码| 亚洲第一级黄色片| aiai在线| 国产精品福利网站| 成人午夜网址| 亚洲成人自拍| 免费久久99精品国产自在现线| 一级片黄色免费| 久久久国产午夜精品| 日韩av在线看免费观看| 亚洲午夜精品一区二区三区他趣| 午夜精品一区二| 亚洲韩国青草视频| 黄色网页在线观看| 国产精品美女免费看| 欧美人妖在线| 成 年 人 黄 色 大 片大 全| 国产一区二区伦理| 国产传媒在线看| 色综合久久九月婷婷色综合| 精品人妻一区二区三区浪潮在线| 久久九九热免费视频| 日韩视频网站在线观看| 国产一区在线观| 国产在线日韩| www.偷拍.com| 国产精品免费网站在线观看| 欧美一级做a爰片免费视频| 精品亚洲一区二区三区在线观看 | 精久久久久久久久久久| 这里只有久久精品| 精品日韩美女的视频高清 | 精品国产乱码久久久久久牛牛| 麻豆影视在线观看_| 欧美亚洲另类视频| 九九热线有精品视频99| 亚洲午夜无码av毛片久久| 99久久er热在这里只有精品15| 一区二区三区免费高清视频| 欧美一级国产精品| 黄色在线免费看| 97se国产在线视频| 欧美午夜不卡| 91超薄肉色丝袜交足高跟凉鞋| 亚洲欧美日韩电影| 99免费在线视频| 91精品国产91久久久| 国产亚洲精品美女久久| 国产一线二线三线女| 成人的网站免费观看| 日韩欧美性视频| 亚洲精品按摩视频| 欧美aa在线| 噜噜噜噜噜久久久久久91| 亚洲女同同性videoxma| 国产麻豆天美果冻无码视频| 色综合天天在线| 国产成人在线视频免费观看| 91精品国产一区二区三区动漫 | 欧美www视频| 日韩脚交footjobhdboots| 欧美主播一区二区三区美女 久久精品人 | 秋霞在线午夜| 精品国产一区二区三区麻豆免费观看完整版 | 精品国产sm最大网站免费看| mm视频在线视频| 鲁丝片一区二区三区| 日韩精品午夜视频| 日韩在线观看免| 精品国产一区久久| 国模视频一区| 黄色影视在线观看| 99热这里都是精品| 日本妇乱大交xxxxx| 久久久精品免费视频|