偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

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

具體而言:

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

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

panic 是什么

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

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

我們來看一個(gè)簡單的例子:

package main

import"fmt"

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

func triggerPanic() {
    defer fmt.Println("defer in triggerPanic: 1") // 這個(gè) defer 會(huì)在 panic 發(fā)生后執(zhí)行
    fmt.Println("triggerPanic 函數(shù)執(zhí)行中...")
    var nums []int
    // 嘗試訪問一個(gè) nil 切片的元素,這將引發(fā)運(yùn)行時(shí) panic
    fmt.Println(nums[0]) // 這里會(huì) panic
    defer fmt.Println("defer in triggerPanic: 2") // 這個(gè) defer 不會(huì)執(zhí)行,因?yàn)樗?panic 之后
    fmt.Println("triggerPanic 函數(shù)即將結(jié)束 - 這行不會(huì)被執(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]) 會(huì)因?yàn)閷?nbsp;nil 切片進(jìn)行索引操作而觸發(fā)一個(gè)運(yùn)行時(shí) panic。一旦 panic 發(fā)生:

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

defer 是什么?

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

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

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

考慮下面的代碼示例:

package main

import"fmt"

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

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

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

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

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

recover 是什么?

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

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

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

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

一個(gè)典型的使用 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() {
        // 這個(gè)匿名函數(shù)是一個(gè) defer 函數(shù)
        if r := recover(); r != nil {
            // r 是 panic 傳遞過來的值
            fmt.Printf("捕獲到 panic: %v\n", r)
            fmt.Println("程序已從 panic 中恢復(fù),繼續(xù)執(zhí)行...")
        }
    }() // 注意這里的 (),表示定義并立即調(diào)用該匿名函數(shù)(實(shí)際上是注冊)

    fmt.Printf("嘗試 %d / %d\n", a, b)
    if b == 0 {
        panic("除數(shù)為零!") // 主動(dòng) 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é)束

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

panic/defer/recover 的交互流程

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

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

package main

import"fmt"

func C(level int) {
    fmt.Printf("進(jìn)入 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("進(jìn)入 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ù),這里也不會(huì)執(zhí)行,因?yàn)?defer 在之后調(diào)用")
}

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

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

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

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

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

g (Goroutine)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

調(diào)用流程(函數(shù)返回或 panic 時(shí))

當(dāng)包含 defer 語句的函數(shù)即將返回(無論是正常返回還是因?yàn)?nbsp;panic)時(shí),運(yùn)行時(shí)會(huì)檢查當(dāng)前 goroutine 的 _defer 鏈表。這個(gè)過程由 runtime.deferreturn(或類似函數(shù))處理:

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

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

將保存在 _defer 記錄中的參數(shù)復(fù)制到當(dāng)前棧幀,為調(diào)用做準(zhǔn)備。

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

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

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

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

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

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

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

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

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

Go 1.22 中的變化

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

總結(jié):

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

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

_panic 的傳播流程與內(nèi)部細(xì)節(jié)

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

其大致流程如下:

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

匯編層面與棧展開的理解

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

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

recover 的實(shí)現(xiàn)

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

recover 的執(zhí)行流程:

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

為什么 recover 要放在 defer 中?

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

嵌套 panic 的情況

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

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

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

總結(jié)

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

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

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

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

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

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

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

panic / recover 使用場景

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

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

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

  • recover 的合理場景 :

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

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

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

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

2025-06-03 02:00:00

2013-06-25 09:52:32

GoGo語言Go編程

2015-12-28 11:25:51

C++異常處理機(jī)制

2011-03-17 09:20:05

異常處理機(jī)制

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ù)庫處理機(jī)制

2025-05-28 03:00:00

2023-10-09 07:14:42

panicGo語言

2011-07-21 15:20:41

java異常處理機(jī)制

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#異常處理機(jī)制

2021-07-03 17:53:52

Java異常處理機(jī)制

2021-03-02 09:12:25

Java異常機(jī)制

2024-02-27 10:48:16

C++代碼開發(fā)

2023-06-15 14:09:00

解析器Servlet容器
點(diǎn)贊
收藏

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