go中panic源碼解讀
panic源碼解讀
前言
本文是在go version go1.13.15 darwin/amd64
上進行的
panic的作用
-
panic
能夠改變程序的控制流,調用panic
後會立刻停止執行當前函數的剩餘代碼,並在當前Goroutine
中遞歸執行調用方的defer
; -
recover
可以中止panic
造成的程序崩潰。它是一個只能在defer
中發揮作用的函數,在其他作用域中調用不會發揮作用;
舉個栗子
package main
import "fmt"
func main() {
fmt.Println(1)
func() {
fmt.Println(2)
panic("3")
}()
fmt.Println(4)
}
輸出
1
2
panic: 3
goroutine 1 [running]:
main.main.func1(...)
/Users/yj/Go/src/Go-POINT/panic/main.go:9
main.main()
/Users/yj/Go/src/Go-POINT/panic/main.go:10 +0xee
panic
後會立刻停止執行當前函數的剩餘代碼,所以4沒有打印出來
對於recover
-
panic只會觸發當前Goroutine的defer;
-
recover只有在defer中調用才會生效;
-
panic允許在defer中嵌套多次調用;
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(1)
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
go func() {
fmt.Println(2)
panic("3")
}()
time.Sleep(time.Second)
fmt.Println(4)
}
上面的栗子,因為recover
和panic
不在同一個goroutine
中,所以不會捕獲到
嵌套的demo
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("3 panic again and again")
}()
panic("2 panic again")
}()
panic("1 panic once")
}
輸出
in main
panic: 1 panic once
panic: 2 panic again
panic: 3 panic again and again
goroutine 1 [running]:
...
多次調用panic
也不會影響defer
函數的正常執行,所以使用defer
進行收尾工作一般來說都是安全的。
panic使用場景
-
error:可預見的錯誤
-
panic:不可預見的異常
需要注意的是,你應該儘可能地使用error
,而不是使用panic
和recover
。只有當程序不能繼續運行的時候,才應該使用panic
和recover
機制。
panic
有兩個合理的用例。
1、發生了一個不能恢復的錯誤,此時程序不能繼續運行。 一個例子就是 web 服務器無法綁定所要求的端口。在這種情況下,就應該使用 panic,因為如果不能綁定端口,啥也做不了。
2、發生了一個編程上的錯誤。 假如我們有一個接收指針參數的方法,而其他人使用 nil 作為參數調用了它。在這種情況下,我們可以使用panic,因為這是一個編程錯誤:用 nil 參數調用了一個只能接收合法指針的方法。
在一般情況下,我們不應通過調用panic函數來報告普通的錯誤,而應該只把它作為報告致命錯誤的一種方式。當某些不應該發生的場景發生時,我們就應該調用panic。
總結下panic
的使用場景:
-
1、空指針引用
-
2、下標越界
-
3、除數為0
-
4、不應該出現的分支,比如default
-
5、輸入不應該引起函數錯誤
看下實現
先來看下_panic
的結構
// _panic 保存了一個活躍的 panic
//
// 這個標記了 go:notinheap 因為 _panic 的值必須位於棧上
//
// argp 和 link 字段為棧指針,但在棧增長時不需要特殊處理:因為他們是指針類型且
// _panic 值只位於棧上,正常的棧指針調整會處理他們。
//
//go:notinheap
type _panic struct {
argp unsafe.Pointer // panic 期間 defer 調用參數的指針; 無法移動 - liblink 已知
arg interface{} // panic的參數
link *_panic // link 鏈接到更早的 panic
recovered bool // panic是否結束
aborted bool // panic是否被忽略
}
link
指向了保存在goroutine
鏈表中先前的panic
鏈表
gopanic
編譯器會將panic
裝換成gopanic
,來看下執行的流程:
1、創建新的runtime._panic
並添加到所在Goroutine
的_panic
鏈表的最前面;
2、在循環中不斷從當前Goroutine 的_defer
中鏈表獲取runtime._defer
並調用runtime.reflectcall
運行延遲調用函數;
3、調用runtime.fatalpanic
中止整個程序;
// 預先聲明的函數 panic 的實現
func gopanic(e interface{}) {
gp := getg()
// 判斷在系統棧上還是在用戶棧上
// 如果執行在系統或信號棧時,getg() 會返回當前 m 的 g0 或 gsignal
// 因此可以通過 gp.m.curg == gp 來判斷所在棧
// 系統棧上的 panic 無法恢復
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
// 如果正在進行 malloc 時發生 panic 也無法恢復
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
// 在禁止搶佔時發生 panic 也無法恢復
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
// 在 g 鎖在 m 上時發生 panic 也無法恢復
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
// 下面是可以恢復的
var p _panic
p.arg = e
// panic 保存了對應的消息,並指向了保存在 goroutine 鏈表中先前的 panic 鏈表
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
for {
// 開始逐個取當前 goroutine 的 defer 調用
d := gp._defer
// 沒有defer,退出循環
if d == nil {
break
}
// 如果 defer 是由早期的 panic 或 Goexit 開始的(並且,因為我們回到這裡,這引發了新的 panic),
// 則將 defer 帶離鏈表。更早的 panic 或 Goexit 將無法繼續運行。
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
// 將deferred標記為started
// 如果棧增長或者垃圾回收在 reflectcall 開始執行 d.fn 前發生
// 標記 defer 已經開始執行,但仍將其保存在列表中,從而 traceback 可以找到並更新這個 defer 的參數幀
// 標記defer是否已經執行
d.started = true
// 記錄正在運行的延遲的panic。
// 如果在延遲調用期間有新的panic,那麼這個panic
// 將在列表中找到d,並將標記d._panic(此panic)中止。
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil
// reflectcall沒有panic。刪除d
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC()
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// 忽略的 panic 會被標記,但仍然保留在 g.panic 列表中
// 這裡將它們移出列表
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // 必須由 signal 完成
gp.sig = 0
}
// 傳遞關於恢復幀的信息
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 調用 recover,並重新進入調度循環,不再返回
mcall(recovery)
// 如果無法重新進入調度循環,則無法恢復錯誤
throw("recovery failed") // mcall should not return
}
}
// 消耗完所有的 defer 調用,保守地進行 panic
// 因為在凍結之後調用任意用戶代碼是不安全的,所以我們調用 preprintpanics 來調用
// 所有必要的 Error 和 String 方法來在 startpanic 之前準備 panic 字符串。
preprintpanics(gp._panic)
fatalpanic(gp._panic) // 不應該返回
*(*int)(nil) = 0 // 無法觸及
}
// reflectcall 使用 arg 指向的 n 個參數位元組的副本調用 fn。
// fn 返回後,reflectcall 在返回之前將 n-retoffset 結果位元組複製回 arg+retoffset。
// 如果重新複製結果位元組,則調用者應將參數幀類型作為 argtype 傳遞,以便該調用可以在複製期間執行適當的寫障礙。
// reflect 包傳遞幀類型。在 runtime 包中,只有一個調用將結果複製回來,即 cgocallbackg1,
// 並且它不傳遞幀類型,這意味着沒有調用寫障礙。參見該調用的頁面了解相關理由。
//
// 包 reflect 通過 linkname 訪問此符號
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)
梳理下流程
1、在處理panic
期間,會先判斷當前panic
的類型,確定panic
是否可恢復;
- 系統棧上的panic無法恢復
- 如果正在進行malloc時發生panic也無法恢復
- 在禁止搶佔時發生panic也無法恢復
- 在g鎖在m上時發生panic也無法恢復
2、可恢復的panic
,panic
的link
指向goroutine
鏈表中先前的panic
鏈表;
3、循環逐個獲取當前goroutine
的defer
調用;
-
如果defer是由早期panic或Goexit開始的,則將defer帶離鏈表,更早的panic或Goexit將無法繼續運行,也就是將之前的panic終止掉,將aborted設置為true,在下面執行recover時保證goexit不會被取消;
-
recovered會在gorecover中被標記,見下文。當recovered被標記為true時,recovery函數觸發Goroutine的調度,調度之前會準備好 sp、pc 以及函數的返回值;
-
當延遲函數中
recover
了一個panic
時,就會返回1,當runtime.deferproc
函數的返回值是1時,編譯器生成的代碼會直接跳轉到調用方函數返回之前並執行runtime.deferreturn
,跳轉到runtime.deferturn
函數之後,程序就已經從panic
恢復了正常的邏輯。而runtime.gorecover
函數也能從runtime._panic
結構中取出了調用panic
時傳入的arg
參數並返回給調用方。
// 在發生 panic 後 defer 函數調用 recover 後展開棧。然後安排繼續運行,
// 就像 defer 函數的調用方正常返回一樣。
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// d's arguments need to be in the stack.
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
}
// 使 deferproc 為此 d 返回
// 這時候返回 1。調用函數將跳轉到標準的返回尾聲
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
在recovery
函數中,利用g
中的兩個狀態碼回溯棧指針sp
並恢復程序計數器pc
到調度器中,並調用gogo
重新調度g
,將g
恢復到調用recover
函數的位置,goroutine
繼續執行,recovery
在調度過程中會將函數的返回值設置為1。調用函數將跳轉到標準的返回尾聲。
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
...
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
當延遲函數中recover
了一個panic
時,就會返回1,當runtime.deferproc
函數的返回值是1時,編譯器生成的代碼會直接跳轉到調用方函數返回之前並執行runtime.deferreturn
,跳轉到runtime.deferturn
函數之後,程序就已經從panic
恢復了正常的邏輯。而runtime.gorecover
函數也能從runtime._panic
結構中取出了調用panic
時傳入的arg
參數並返回給調用方。
gorecover
編譯器會將recover
裝換成gorecover
如果recover
被正確執行了,也就是gorecover
,那麼recovered
將被標記成true
// go/src/runtime/panic.go
// 執行預先聲明的函數 recover。
// 不允許分段棧,因為它需要可靠地找到其調用者的棧段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// 必須在 panic 期間作為 defer 調用的一部分在函數中運行。
// 必須從調用的最頂層函數( defer 語句中使用的函數)調用。
// p.argp 是最頂層 defer 函數調用的參數指針。
// 比較調用方報告的 argp,如果匹配,則調用者可以恢復。
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
// 標記recovered
p.recovered = true
return p.arg
}
return nil
}
在正常情況下,它會修改runtime._panic
的recovered
字段,runtime.gorecover
函數中並不包含恢復程序的邏輯,程序的恢復是由runtime.gopanic
函數負責。
gorecover
將recovered
標記為true,然後gopanic
就可以通過mcall
調用recovery
並重新進入調度循環
fatalpanic
runtime.fatalpanic
實現了無法被恢復的程序崩潰,它在中止程序之前會通過runtime.printpanics
打印出全部的panic
消息以及調用時傳入的參數:
// go/src/runtime/panic.go
// fatalpanic 實現了不可恢復的 panic。類似於 fatalthrow,
// 如果 msgs != nil,則 fatalpanic 仍然能夠打印 panic 的消息
// 並在 main 在退出時候減少 runningPanicDeferss
//
//go:nosplit
func fatalpanic(msgs *_panic) {
// 返回程序計數寄存器指針
pc := getcallerpc()
// 返回堆棧指針
sp := getcallersp()
// 返回當前G
gp := getg()
var docrash bool
// 切換到系統棧來避免棧增長,如果運行時狀態較差則可能導致更糟糕的事情
systemstack(func() {
if startpanic_m() && msgs != nil {
// 有 panic 消息和 startpanic_m 則可以嘗試打印它們
// startpanic_m 設置 panic 會從阻止 main 的退出,
// 因此現在可以開始減少 runningPanicDefers 了
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
// 通過在上述 systemstack 調用之外崩潰,調試器在生成回溯時不會混淆。
// 函數崩潰標記為 nosplit 以避免堆棧增長。
crash()
}
// 從系統推出
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}
// 打印出當前活動的panic
func printpanics(p *_panic) {
if p.link != nil {
printpanics(p.link)
print("\t")
}
print("panic: ")
printany(p.arg)
if p.recovered {
print(" [recovered]")
}
print("\n")
}
總結
引一段來自【panic 和recover】的總結
1、編譯器會負責做轉換關鍵字的工作;
-
1、將
panic
和recover
分別轉換成runtime.gopanic
和runtime.gorecover
; -
2、將
defer
轉換成runtime.deferproc
函數; -
3、在調用
defer
的函數末尾調用runtime.deferreturn
函數;
2、在運行過程中遇到runtime.gopanic
方法時,會從Goroutine
的鏈表依次取出runtime._defer
結構體並執行;
3、如果調用延遲執行函數時遇到了runtime.gorecover
就會將_panic.recovered
標記成true
並返回panic
的參數;
-
1、在這次調用結束之後,
runtime.gopanic
會從runtime._defer
結構體中取出程序計數器pc
和棧指針sp
並調用runtime.recovery
函數進行恢復程序; -
2、
runtime.recovery
會根據傳入的pc
和sp
跳轉回runtime.deferproc
; -
3、編譯器自動生成的代碼會發現
runtime.deferproc
的返回值不為0
,這時會跳回runtime.deferreturn
並恢復到正常的執行流程;
4、如果沒有遇到runtime.gorecover
就會依次遍歷所有的runtime._defer
,並在最後調用runtime.fatalpanic
中止程序、打印panic
的參數並返回錯誤碼2
;
參考
【panic 和 recover】//draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/
【恐慌與恢復內建函數】//golang.design/under-the-hood/zh-cn/part1basic/ch03lang/panic/
【Go語言panic/recover的實現】//zhuanlan.zhihu.com/p/72779197
【panic and recover】//eddycjy.gitbook.io/golang/di-6-ke-chang-yong-guan-jian-zi/panic-and-recover#yuan-ma
【翻了源碼,我把 panic 與 recover 給徹底搞明白了】//jishuin.proginn.com/p/763bfbd4ed8c