成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

具體而言:

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

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

panic 是什么

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

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

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

package main

import"fmt"

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

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ù)即將結(jié)束 - 這行不會被執(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("程序結(jié)束 - 這行不會被執(zhí)行") 以及 triggerPanic 函數(shù)中 panic 點之后的代碼都不會執(zhí)行。

defer 是什么?

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

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

另一個關(guān)鍵特性是,如果一個函數(shù)內(nèi)有多個 defer 語句,它們的執(zhí)行順序是“先進后出”(LIFO)。也就是說,最先被 defer 的函數(shù)調(diào)用最后執(zhí)行,最后被 defer 的函數(shù)調(diào)用最先執(zhí)行,就像一個棧結(jié)構(gòu)。

考慮下面的代碼示例:

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: 結(jié)束")
}
main: 開始
main: value 最終為 3
main: 結(jié)束
第二個 defer, value = 2
第一個 defer, value = 1

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

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

recover 是什么?

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

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

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

可以認為,recover 給予了程序一個在發(fā)生災(zāi)難性錯誤時進行“自救”的機會。它允許程序捕獲 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ù)結(jié)束")
}

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 中恢復(fù),繼續(xù)執(zhí)行...")
        }
    }() // 注意這里的 (),表示定義并立即調(diào)用該匿名函數(shù)(實際上是注冊)

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

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

panic/defer/recover 的交互流程

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

假設(shè)我們有如下函數(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 中恢復(fù): %v\n", r)
        }
        fmt.Println("defer in B (結(jié)束)")
    }()

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

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

func main() {
    fmt.Println("main: 開始")
    A()
    fmt.Println("=== 分割線 ===")
    B()
    fmt.Println("main: 結(jié)束")
}
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 中恢復(fù): 在 C (層級 1) 中發(fā)生 panic
defer in B (結(jié)束)
main: 結(jié)束

實現(xiàn)原理與數(shù)據(jù)結(jié)構(gòu)

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

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

g (Goroutine)

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

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

_defer 結(jié)構(gòu)體

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

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

_panic 結(jié)構(gòu)體

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

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

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

_defer 的入棧與調(diào)用流程

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

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

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

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

調(diào)用流程(函數(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ù)復(fù)制到當前棧幀,為調(diào)用做準備。

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

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

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

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

  • 為什么推薦在 defer 后接一個閉包?
  • 訪問外層函數(shù)作用域 :閉包可以捕獲其定義時所在作用域的變量。這使得 defer 的邏輯可以方便地與外層函數(shù)的狀態(tài)交互,例如修改命名返回值,或者訪問在 defer 語句時尚未聲明但在函數(shù)返回前會賦值的變量。
  • 執(zhí)行復(fù)雜邏輯 :如果 defer 需要執(zhí)行的不僅僅是一個簡單的函數(shù)調(diào)用,而是一系列操作,閉包提供了一種簡潔的方式來封裝這些操作。
  • 正確處理循環(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)里,分別調(diào)用了接收者為值的方法 hello 和接收者為指針的方法 hello2。按 Go 的規(guī)范,每一個 defer 語句都會生成一個“閉包”(closure),而這個閉包會 捕獲(capture) 循環(huán)變量 t。下面分兩部分來詳細說明其行為差異:

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

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

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

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

Go 1.22 中的變化

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

總結(jié):

  • 值接收者 的 defer 總是捕獲當時的值拷貝,跟循環(huán)變量的重寫行為無關(guān);
  • 指針接收者 的 defer 捕獲的是循環(huán)變量的地址,若循環(huán)變量重用同一地址(如 Go 1.21 及以前版本),所有閉包共用最終那次迭代的內(nèi)容;
  • 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 的傳播流程與內(nèi)部細節(jié)

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

其大致流程如下:

  1. 創(chuàng)建 _panic 記錄
  • 運行時系統(tǒng)首先創(chuàng)建一個 _panic 結(jié)構(gòu)體實例。
  • 這個結(jié)構(gòu)體的 arg 字段會被設(shè)置為傳遞給 panic 的值 v。
  • link 字段會指向當前 goroutine (g) 可能已經(jīng)存在的 _panic 記錄(g._panic)。這種情況發(fā)生在 defer 函數(shù)執(zhí)行過程中又觸發(fā)了新的 panic(嵌套 panic)。新 panic 會覆蓋舊 panic,舊的 panic 信息會通過 link 鏈起來。
  • recovered 字段初始化為 false。
  • 新創(chuàng)建的 _panic 記錄會被設(shè)置為當前 goroutine 的活動 panic,即 g._panic 指向這個新記錄。
  1. 開始棧展開(Stack Unwinding)與執(zhí)行 defer
  • 對應(yīng)的延遲函數(shù)被調(diào)用。
  • 關(guān)鍵點 :如果在這個延遲函數(shù)內(nèi)部直接調(diào)用了 recover(),并且 recover() 成功捕獲了當前的 panic(即 g._panic 所指向的 panic),那么 g._panic.recovered 標記會被設(shè)為 true。gopanic 函數(shù)會注意到這個標記,停止繼續(xù)展開 _defer 鏈,并開始執(zhí)行恢復(fù)流程(見下一節(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 調(diào)用捕獲),那么 gopanic 會調(diào)用 runtime.fatalpanic。
  • runtime.fatalpanic 會打印出當前的 panic 值 (g._panic.arg) 和發(fā)生 panic 時的調(diào)用堆棧信息。
  • 最后,程序會以非零狀態(tài)碼退出,通常是2。

匯編層面與棧展開的理解

雖然我們通常不直接接觸匯編,但理解其概念有助于明白“棧展開”。當一個函數(shù)調(diào)用另一個函數(shù)時,返回地址、參數(shù)、局部變量等會被壓入當前 goroutine 的棧。發(fā)生 panic 時,gopanic 的過程實際上就是在模擬函數(shù)返回的過程,但它不是正常返回,而是逐個“彈出”棧幀(邏輯上),并查找與這些棧幀關(guān)聯(lián)的 _defer 記錄來執(zhí)行。如果 panic 未被 recover,這個展開過程會一直持續(xù)到 goroutine 棧的最初始調(diào)用者,最終導(dǎo)致程序終止。這個過程由運行時系統(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. 檢查調(diào)用上下文 :gorecover 首先會檢查它是否在正確的上下文中被調(diào)用。最關(guān)鍵的檢查是當前 goroutine (g) 是否正處于 panic 狀態(tài)(即 g._panic != nil)并且這個 panic 尚未被標記為 recovered(g._panic.recovered == false)。
  • 如果 g._panic 為 nil(沒有活動的 panic),或者 g._panic.recovered 為 true(panic 已經(jīng)被其他 recover 調(diào)用處理過了),那么 gorecover 直接返回 nil。這解釋了為什么在沒有 panic 的情況下調(diào)用 recover 會返回 nil。
  1. 檢查是否直接在 defer 函數(shù)中調(diào)用 :Go 運行時還需要確保 recover 是被 defer 調(diào)用的函數(shù)直接調(diào)用的,而不是在 defer 函數(shù)調(diào)用的更深層函數(shù)中調(diào)用。這是通過比較調(diào)用 gorecover 時的棧指針 (argp,它指向 recover 函數(shù)的參數(shù)在棧上的位置) 與 g._defer 鏈表頭記錄的棧指針 (d.sp) 是否匹配。
  • 如果棧指針不匹配,意味著 recover 不是在最頂層的 defer 函數(shù)(即當前正在執(zhí)行的 defer)中直接調(diào)用的,這種情況下 gorecover 也會返回 nil。這就是“recover 必須直接在 defer 函數(shù)中調(diào)用”規(guī)則的由來。
  1. 標記 panic 為已恢復(fù) :如果上述檢查都通過,說明 recover 是在合法的時機和位置被調(diào)用的:
  • gorecover 會將當前活動的 panic(即 g._panic)的 recovered 字段標記為 true。
  • 它會保存 panic 的參數(shù)值 (g._panic.arg)。
  1. 清除當前 panic :為了防止后續(xù)的 defer 或同一個 defer 中的其他 recover 再次處理同一個 panic,gorecover 會將 g._panic 設(shè)置為 nil(或者在有嵌套 panic 的情況下,將其設(shè)置為 g._panic.link,即恢復(fù)到上一個 panic 的狀態(tài))。實際上,在 gopanic 的循環(huán)中,當它檢測到 recovered 標志被設(shè)為 true 后,它會負責清理 g._panic 并調(diào)整控制流以正常返回。
  2. 返回 panic 的參數(shù) :最后,gorecover 返回之前保存的 panic 參數(shù)值。調(diào)用者(即 defer 函數(shù)中的代碼)可以通過檢查這個返回值是否為 nil 來判斷是否成功捕獲了 panic。

為什么 recover 要放在 defer 中?

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

嵌套 panic 的情況

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

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

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

總結(jié)

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

defer 對性能的影響與技術(shù)取舍

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

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

設(shè)計哲學(xué)

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

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

panic / recover 使用場景

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

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

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

  • recover 的合理場景 :

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

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

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

責任編輯:武曉燕 來源: Piper蛋窩
相關(guān)推薦

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語言

2011-04-06 10:27:46

Java異常處理

2024-03-04 10:00:35

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

2025-05-28 03:00:00

2023-10-09 07:14:42

panicGo語言

2011-07-21 15:20:41

java異常處理機制

2025-05-22 09:01:28

2023-03-08 08:54:59

SpringMVCJava

2025-03-31 08:57:25

Go程序性能

2010-03-05 15:40:16

Python異常

2009-08-05 18:09:17

C#異常處理機制

2021-07-03 17:53:52

Java異常處理機制

2021-03-02 09:12:25

Java異常機制

2024-02-27 10:48:16

C++代碼開發(fā)

2023-06-15 14:09:00

解析器Servlet容器
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 99热精品久久 | 久久综合久 | 日韩高清中文字幕 | 日本天堂一区二区 | 成人午夜免费视频 | 国产一级视频免费播放 | 国产高清精品网站 | 久久综合色综合 | 91成人午夜性a一级毛片 | 久久三区 | 日韩精品一区二区在线 | 久久久久久美女 | 成人午夜高清 | 亚洲精品视频二区 | 99久久精品免费看国产高清 | 999热精品| 亚洲精品99 | 天天干天天想 | 免费同性女女aaa免费网站 | 欧美日韩综合一区 | 亚洲精品一区二区三区中文字幕 | 色综合桃花网 | 自拍偷拍亚洲欧美 | 成人免费观看男女羞羞视频 | 国产三区四区 | 免费麻豆视频 | 国产精品久久久久久久久久尿 | 中文字幕成人 | 九九99久久 | 日韩欧美一级片 | 911网站大全在线观看 | 最新国产在线 | 欧美激情精品久久久久久变态 | 国产婷婷色一区二区三区 | 久久久www成人免费无遮挡大片 | 午夜精品久久久久久久99黑人 | 福利视频一区二区三区 | 久久久久久国产一区二区三区 | 91秦先生艺校小琴 | 五月天综合影院 | 日本一区二区高清视频 |