panic
panic是 go 的內置函數,當遇到不可恢復的錯誤時,程序往往就會拋出panic,比如常見的空指針訪問
func main() {
var a *int
*a = 1
}運行上面這段代碼,程序就會拋出如下的panic,然後程序就會停止。
panic: runtime error: invalid memory address or nil pointer dereference在一些情況下,我們也會手動調用panic函數來讓程序退出,從而避免更嚴重的後果。平時也會用另一個內置函數recover來捕獲panic,並配合defer使用。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}為什麼recover函數一定要在defer裡面使用,recover做了些什麼工作,這些問題都會在下面的內容得到解答。
結構
panic在運行時也有對應的結構進行表示,那就是runtime._panic,其結構並不復雜,如下。
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg any // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}它的結構與defer非常類似,
link指向下一個_panic結構,pc和sp指向調用函數的執行現場便於日後恢復,arg就是panic函數的參數,argp指向defer的參數,panic發生後便會觸發defer的執行aborted表示其是否被強制停止
panic跟defer一樣,也是以鏈表的形式存在於協程中
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
恐慌
無論是我們主動調用panic函數,抑或是程序發生的panic,最終都會走入runtime.gopanic函數中
func gopanic(e any)在開始時,首先會檢測參數是否為nil,如果是nil的話就會 new 一個runtime.PanicNilError類型的錯誤
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}然後將當前的panic加入協程的鏈表頭部
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))隨後進入for循環開始逐個處理當前協程的defer鏈表
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
}如果當前的defer已經被其它的panic觸發了,即_defer.started == true,那麼較早的panic將不會執行。然後再執行defer對應的函數
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)執行完後回收當前的defer結構,繼續執行下一個defer,當執行完全部的defer結構後且期間沒有被恢復,就會進入runtime.fatalpanic函數,該函數是unrecoverable即不可恢復的
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
runningPanicDefers.Add(-1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}在這期間會讓printpanics打印panic的信息,我們通常看到的調用棧信息就是由它輸出的,最後由runtime.exit函數通過系統調用_ExitProcess退出程序。
恢復
通過調用內置函數recover,編譯期間會變為對runtime.gorecover函數的調用
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}它的實現非常簡單,只干了p.recovered = true這麼一件事,而真正負責處理恢復邏輯的代碼實際上在gopanic函數裡
for {
...
d.fn()
...
if p.recovered {
...
}
}恢復的邏輯在defer執行後,到這裡也就明白了為什麼recover函數只能在defer中使用,如果在defer之外使用recover函數的話gp._panic就等於nil,自然p.recovered就不會被設置為true,那麼在gopanic函數中也就不會走到恢復這部分邏輯裡面來。
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}恢復時會清理鏈表中那些已經被強制停止的panic,然後進入runtime.recovery函數中,由runtime.gogo回到用戶函數的正常邏輯流程中
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}然後有一個重點需要注意的是這行代碼
gp.sched.ret = 1它將ret值設置成為了 1,從runtime.deferproc的函數注釋中可以看代碼下面這些內容
func deferproc(fn func()) {
...
// 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()
}編譯器生成的中間代碼會檢查該值是否為 1,如果是的話就會直接執行runtime.deferreturn函數,通常該函數只有在函數返回之前才會執行,這也說明了為什麼recover過後函數會直接返回。
