翻了源碼,我把 panic 與 recover 給徹底搞明白了
點(diǎn)擊上方“Go編程時(shí)光”,選擇“加為星標(biāo)”
第一時(shí)間關(guān)注Go技術(shù)干貨!

0. 寫在前面
今天與大家來聊一聊go語言中的"throw、try…..catch{}"。如果你之前是一名java程序員,我相信你一定吐槽過go語言錯(cuò)誤處理方式,但是這篇文章不是來討論好壞的,我們本文的重點(diǎn)是帶著大家看一看panic與recover是如何實(shí)現(xiàn)的。上一文我們講解了defer是如何實(shí)現(xiàn)的,但是沒有講解與defer緊密相連的recover,想搞懂panic與recover的實(shí)現(xiàn)也沒那么簡(jiǎn)單,就放到這一篇來講解了。廢話不多說,直接開整。
1. 什么是`panic`、`recover`
Go 語言中 panic 關(guān)鍵字主要用于主動(dòng)拋出異常,類似 java 等語言中的 throw 關(guān)鍵字。panic 能夠改變程序的控制流,調(diào)用 panic 后會(huì)立刻停止執(zhí)行當(dāng)前函數(shù)的剩余代碼,并在當(dāng)前 Goroutine 中遞歸執(zhí)行調(diào)用方的 defer;
Go 語言中 recover 關(guān)鍵字主要用于捕獲異常,讓程序回到正常狀態(tài),類似 java 等語言中的 try ... catch 。recover 可以中止 panic 造成的程序崩潰。它是一個(gè)只能在 defer 中發(fā)揮作用的函數(shù),在其他作用域中調(diào)用不會(huì)發(fā)揮作用;
recover只能在defer中使用這個(gè)在標(biāo)準(zhǔn)庫的注釋中已經(jīng)寫明白了,我們可以看一下:
// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}
這里有一個(gè)要注意的點(diǎn)就是recover必須要要在defer函數(shù)中使用,否則無法阻止panic。最好的驗(yàn)證方法是先寫兩個(gè)例子:
func main() {
example1()
example2()
}
func example1() {
defer func() {
if err := recover(); err !=nil{
fmt.Println(string(Stack()))
}
}()
panic("unknown")
}
func example2() {
defer recover()
panic("unknown")
}
func Stack() []byte {
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, false)
if n < len(buf) {
return buf[:n]
}
buf = make([]byte, 2*len(buf))
}
}
運(yùn)行我們會(huì)發(fā)現(xiàn)example2()方法的panic是沒有被recover住的,導(dǎo)致整個(gè)程序直接crash了。這里大家肯定會(huì)有疑問,為什么直接寫recover()就不能阻止panic了呢。我們?cè)?a style="font-size: inherit;line-height: inherit;margin: 0px;padding: 0px;text-decoration: none;color: rgb(30, 107, 184);overflow-wrap: break-word;" data-linktype="2">詳解defer實(shí)現(xiàn)機(jī)制(附上三道面試題,我不信你們都能做對(duì))講解了defer實(shí)現(xiàn)原理,一個(gè)重要的知識(shí)點(diǎn)defer將語句放入到棧中時(shí),也會(huì)將相關(guān)的值拷貝同時(shí)入棧。所以defer recover()這種寫法在放入defer棧中時(shí)就已經(jīng)被執(zhí)行過了,panic是發(fā)生在之后,所以根本無法阻止住panic。
2. 它們的特性
上面我們簡(jiǎn)單的介紹了一下什么是panic與recover,下面我一起來看看他們有什么特性,避免我們踩坑。
recover只有在defer函數(shù)中使用才有效,上面已經(jīng)舉例說明了,這里就不在贅述了。panic允許在defer中嵌套多次調(diào)用.程序多次調(diào)用panic也不會(huì)影響defer函數(shù)的正常執(zhí)行,所以使用defer進(jìn)行收尾工作一般來說都是安全的。寫個(gè)例子驗(yàn)證一下:
func example3() {
defer fmt.Println("this is a example3 for defer use panic")
defer func() {
defer func() {
panic("panic defer 2")
}()
panic("panic defer 1")
}()
panic("panic example3")
}
// 運(yùn)行結(jié)果
this is a example3 for defer use panic
panic: panic example3
panic: panic defer 1
panic: panic defer 2
.......... 省略
通過運(yùn)行結(jié)果可以看出panic不會(huì)影響defer函數(shù)的使用,所以他是安全的。
panic只會(huì)對(duì)當(dāng)前Goroutine的defer有效,還記得我們上一文分析的deferproc函數(shù)嗎?在newdefer中分配_defer結(jié)構(gòu)體對(duì)象的時(shí),會(huì)把分配到的對(duì)象鏈入當(dāng)前goroutine的_defer鏈表的表頭,也就是把延遲調(diào)用函數(shù)與調(diào)用方所在的Goroutine進(jìn)行關(guān)聯(lián)。因此當(dāng)程序發(fā)生panic時(shí)只會(huì)調(diào)用當(dāng)前 Goroutine 的延遲調(diào)用函數(shù)是沒有問題的。寫個(gè)例子驗(yàn)證一下:
func main() {
go example4()
go example5()
time.Sleep(10 * time.Second)
}
func example4() {
fmt.Println("goroutine example4")
defer func() {
fmt.Println("test defer")
}()
panic("unknown")
}
func example5() {
defer fmt.Println("goroutine example5")
time.Sleep(5 * time.Second)
}
// 運(yùn)行結(jié)果
goroutine example4
test defer
panic: unknown
............. 省略部分代碼
這里我開了兩個(gè)協(xié)程,一個(gè)協(xié)程會(huì)發(fā)生panic,導(dǎo)致程序崩潰,但是只會(huì)執(zhí)行自己所在Goroutine的延遲函數(shù),所以正好驗(yàn)證了多個(gè) Goroutine 之間沒有太多的關(guān)聯(lián),一個(gè) Goroutine 在 panic 時(shí)也不應(yīng)該執(zhí)行其他 Goroutine 的延遲函數(shù)。
3. 典型應(yīng)用
其實(shí)我們?cè)趯?shí)際項(xiàng)目開發(fā)中,經(jīng)常會(huì)遇到panic問題, Go 的 runtime 代碼中很多地方都調(diào)用了 panic 函數(shù),對(duì)于不了解 Go 底層實(shí)現(xiàn)的新人來說,這無疑是挖了一堆深坑。我們?cè)趯?shí)際生產(chǎn)環(huán)境中總會(huì)出現(xiàn)panic,但是我們的程序仍能正常運(yùn)行,這是因?yàn)槲覀兊目蚣芤呀?jīng)做了recover,他已經(jīng)為我們兜住底,比如gin,我們看一看他是怎么做的。
先看代碼部分吧:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
...................// 省略
}
}()
c.Next()
}
}
我們?cè)谑褂?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">gin時(shí),第一步會(huì)初始化一個(gè)Engine實(shí)例,調(diào)用Default方法會(huì)把recovery middleware附上,recovery中使用了defer函數(shù),通過recover來阻止panic,當(dāng)發(fā)生panic時(shí),會(huì)返回500錯(cuò)誤碼。這里有一個(gè)需要注意的點(diǎn)是只有主程序中的panic是會(huì)被自動(dòng)recover的,協(xié)程中出現(xiàn)panic會(huì)導(dǎo)致整個(gè)程序crash。還記得我們上面講的第三個(gè)特性嘛,一個(gè)協(xié)程會(huì)發(fā)生panic,導(dǎo)致程序崩潰,但是只會(huì)執(zhí)行自己所在Goroutine的延遲函數(shù),所以正好驗(yàn)證了多個(gè) Goroutine 之間沒有太多的關(guān)聯(lián),一個(gè) Goroutine 在 panic 時(shí)也不應(yīng)該執(zhí)行其他 Goroutine 的延遲函數(shù)。 這就能解釋通了吧, 所以為了程序健壯性,我們應(yīng)該自己主動(dòng)檢查我們的協(xié)程程序,在我們的協(xié)程函數(shù)中添加recover是很有必要的,比如這樣:
func main() {
r := gin.Default()
r.GET("/asong/test/go-panic", func(ctx *gin.Context) {
go func() {
defer func() {
if err := recover();err != nil{
fmt.Println(err)
}
}()
panic("panic")
}()
})
r.Run()
}
如果使用的Gin框架,切記要檢查協(xié)程中是否會(huì)出現(xiàn)panic,否則線上將付出沉重的代價(jià)。非常危險(xiǎn)?。?!
4. 源碼解析
go-version: 1.15.3
我們先來寫個(gè)簡(jiǎn)單的代碼,看看他的匯編調(diào)用:
func main() {
defer func() {
if err:= recover();err != nil{
fmt.Println(err)
}
}()
panic("unknown")
}
執(zhí)行go tool compile -N -l -S main.go就可以看到對(duì)應(yīng)的匯編碼了,我們截取部分片段分析:

上面重點(diǎn)部分就是畫紅線的三處,第一步調(diào)用runtime.deferprocStack創(chuàng)建defer對(duì)象,這一步大家可能會(huì)有疑惑,我上一文忘記講個(gè)這個(gè)了,這里先簡(jiǎn)單概括一下,defer總共有三種模型,編譯一個(gè)函數(shù)里只會(huì)有一種defer模式
第一種,堆上分配(deferproc),基本是依賴運(yùn)行時(shí)來分配"_defer"對(duì)象并加入延遲參數(shù)。在函數(shù)的尾部插入
deferreturn方法來消費(fèi)deferlink。第二種,棧上分配(deferprocStack),基本上跟堆差不多,只是分配方式改為在棧上分配,壓入的函數(shù)調(diào)用棧存有
_defer記錄,編譯器在ssa過程中會(huì)預(yù)留defer空間。第三種,開放編碼模式(open coded),不過是有條件的,默認(rèn)open-coded最多支持8個(gè)defer,超過則取消。在構(gòu)建ssa時(shí)如發(fā)現(xiàn)gcflags有N禁止優(yōu)化的參數(shù) 或者 return數(shù)量 * defer數(shù)量超過了 15不適用open-coded模式。并不能處于循環(huán)中。
按理說我們的版本是1.15+,應(yīng)該使用開放編碼模式呀,但是這里怎么還會(huì)在棧上分配?注意看呀,伙計(jì)們,我在匯編處理時(shí)禁止了編譯優(yōu)化,那肯定不會(huì)走開放編碼模式呀,這個(gè)不是重點(diǎn),我們接著分析上面的匯編。
第二個(gè)紅線在程序發(fā)生panic時(shí)會(huì)調(diào)用runtime.gopanic,現(xiàn)在程序處于panic狀態(tài),在函數(shù)返回時(shí)調(diào)用runtime.deferreturn,也就是調(diào)用延遲函數(shù)處理。上面這一步是主程序執(zhí)行部分,下面我們?cè)诳匆幌卵舆t函數(shù)中的執(zhí)行:

這里最重點(diǎn)的就只有一個(gè),調(diào)用runtime.gorecover,也就是在這一步,對(duì)主程序中的panic進(jìn)行了恢復(fù)了,這就是panic與recover的執(zhí)行過程,接下來我們就仔細(xì)分析一下runtime.gopanic、runtime.gorecover這兩個(gè)方法是如何實(shí)現(xiàn)的!
5.1 _panic結(jié)構(gòu)
在講defer實(shí)現(xiàn)機(jī)制時(shí),我們一起看過defer的結(jié)構(gòu),其中有一個(gè)字段就是_panic,是觸發(fā)defer的作用,我們來看看的panic的結(jié)構(gòu):
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // 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
}
簡(jiǎn)單介紹一下上面的字段:
argp是指向defer調(diào)用時(shí)參數(shù)的指針。arg是我們調(diào)用panic時(shí)傳入的參數(shù)link指向的是更早調(diào)用runtime._panic結(jié)構(gòu),也就是說painc可以被連續(xù)調(diào)用,他們之間形成鏈表recovered表示當(dāng)前runtime._panic是否被recover恢復(fù)aborted表示當(dāng)前的panic是否被強(qiáng)行終止
上面的pc、sp、goexit我們單獨(dú)講一下,runtime包中有一個(gè)Goexit方法,Goext能夠終止調(diào)用它的goroutine,其他的goroutine是不受影響的,goexit也會(huì)在終止goroutine之前運(yùn)行所有延遲調(diào)用函數(shù),Goexit不是一個(gè)panic,所以這些延遲函數(shù)中的任何recover調(diào)用都將返回nil。如果我們?cè)谥骱瘮?shù)中調(diào)用了Goexit會(huì)終止該goroutine但不會(huì)返回func main。由于func main沒有返回,因此程序?qū)⒗^續(xù)執(zhí)行其他gorountine,直到所有其他goroutine退出,程序才會(huì)crash。寫個(gè)簡(jiǎn)單的例子:
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
runtime.Goexit()
}()
go func() {
for true {
fmt.Println("test")
}
}()
runtime.Goexit()
fmt.Println("main")
select {
}
}
運(yùn)行上面的例子你就會(huì)發(fā)現(xiàn),即使在主goroutine中調(diào)用了runtime.Goexit,其他goroutine是沒有任何影響的。所以結(jié)構(gòu)中的pc、sp、goexit三個(gè)字段都是為了修復(fù)runtime.Goexit,這三個(gè)字段就是為了保證該函數(shù)的一定會(huì)生效,因?yàn)槿绻?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">defer中發(fā)生panic,那么goexit函數(shù)就會(huì)被取消,所以才有了這三個(gè)字段做保護(hù)??催@個(gè)例子:
func main() {
maybeGoexit()
}
func maybeGoexit() {
defer func() {
fmt.Println(recover())
}()
defer panic("cancelled Goexit!")
runtime.Goexit()
}
英語好的可以看一看這個(gè):https://github.com/golang/go/issues/29226,這就是上面的一個(gè)例子,這里就不過多解釋了,了解就好。
下面就開始我們的重點(diǎn)吧~。
5.2 gopanic
gopanic的代碼有點(diǎn)長(zhǎng),我們一點(diǎn)一點(diǎn)來分析:
第一部分,判斷
panic類型:
gp := getg()
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
根據(jù)不同的類型判斷當(dāng)前發(fā)生panic錯(cuò)誤,這里沒什么多說的,接著往下看。
第二部分,確保每個(gè)
recover都試圖恢復(fù)當(dāng)前協(xié)程中最新產(chǎn)生的且尚未恢復(fù)的panic
var p _panic // 聲明一個(gè)panic結(jié)構(gòu)
p.arg = e // 把panic傳入的值賦給`arg`
p.link = gp._panic // 指向runtime.panic結(jié)構(gòu)
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
// By calculating getcallerpc/getcallersp here, we avoid scanning the
// gopanic frame (stack scanning is slow...)
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
for {
d := gp._defer // 獲取當(dāng)前gorourine的 defer
if d == nil {
break // 如果沒有defer直接退出了
}
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. An earlier panic will not continue running, but we will make sure below that an
// earlier Goexit does continue running.
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
if !d.openDefer {
// For open-coded defers, we need to process the
// defer again, in case there are any other defers
// to call in the frame (not including the defer
// call that caused the panic).
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
}
// Mark defer as started, but keep on list, so that traceback
// can find and update the defer's argument frame if stack growth
// or a garbage collection happens before reflectcall starts executing d.fn.
d.started = true
// Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
上面的代碼不太好說的部分,我添加了注釋,就不在這解釋一遍了,直接看 d.Started部分,這里的意思是如果defer是由先前的panic或Goexit啟動(dòng)的(循環(huán)處理回到這里,這觸發(fā)了新的panic),將defer從列表中刪除。早期的panic將不會(huì)繼續(xù)運(yùn)行,但我們將確保早期的Goexit會(huì)繼續(xù)運(yùn)行,代碼中的if d._panic != nil{d._panic.aborted =true}就是確保將先前的panic終止掉,將aborted設(shè)置為true,在下面執(zhí)行recover時(shí)保證goexit不會(huì)被取消。
第三部分,
defer內(nèi)聯(lián)優(yōu)化調(diào)用性能
if !d.openDefer {
// For open-coded defers, we need to process the
// defer again, in case there are any other defers
// to call in the frame (not including the defer
// call that caused the panic).
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
done := true
if d.openDefer {
done = runOpenDeferFrame(gp, d)
if done && !d._panic.recovered {
addOneOpenDeferFrame(gp, 0, nil)
}
} else {
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
}
上面的代碼都是截圖片段,這些部分都是為了判斷當(dāng)前defer是否可以使用開發(fā)編碼模式,具體怎么操作的就不展開了。
第四部分,
gopanic中執(zhí)行程序恢復(fù)
在第三部分進(jìn)行defer內(nèi)聯(lián)優(yōu)化選擇時(shí)會(huì)執(zhí)行調(diào)用延遲函數(shù)(reflectcall就是這個(gè)作用),也就是會(huì)調(diào)用runtime.gorecover把recoverd = true,具體這個(gè)函數(shù)的操作留在下面講,因?yàn)?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">runtime.gorecover函數(shù)并不包含恢復(fù)程序的邏輯,程序的恢復(fù)是在gopanic中執(zhí)行的。先看一下代碼:
if p.recovered { // 在runtime.gorecover中設(shè)置為true
gp._panic = p.link
if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
// A normal recover would bypass/abort the Goexit. Instead,
// we return to the processing loop of the Goexit.
gp.sigcode0 = uintptr(gp._panic.sp)
gp.sigcode1 = uintptr(gp._panic.pc)
mcall(recovery)
throw("bypassed recovery failed") // mcall should not return
}
atomic.Xadd(&runningPanicDefers, -1)
if done {
// Remove any remaining non-started, open-coded
// defer entries after a recover, since the
// corresponding defers will be executed normally
// (inline). Any such entry will become stale once
// we run the corresponding defers inline and exit
// the associated stack frame.
d := gp._defer
var prev *_defer
for d != nil {
if d.openDefer {
if d.started {
// This defer is started but we
// are in the middle of a
// defer-panic-recover inside of
// it, so don't remove it or any
// further defer entries
break
}
if prev == nil {
gp._defer = d.link
} else {
prev.link = d.link
}
newd := d.link
freedefer(d)
d = newd
} else {
prev = d
d = d.link
}
}
}
gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig = 0
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall should not return
}
這段代碼有點(diǎn)長(zhǎng),主要就是分為兩部分:
第一部分主要是這個(gè)判斷if gp._panic != nil && gp._panic.goexit && gp._panic.aborted { ... },正常recover是會(huì)繞過Goexit的,所以為了解決這個(gè),添加了這個(gè)判斷,這樣就可以保證Goexit也會(huì)被recover住,這里是通過從runtime._panic中取出了程序計(jì)數(shù)器pc和棧指針sp并且調(diào)用runtime.recovery函數(shù)觸發(fā)goroutine的調(diào)度,調(diào)度之前會(huì)準(zhǔn)備好 sp、pc 以及函數(shù)的返回值。
第二部分主要是做panic的recover,這也與上面的流程基本差不多,他是從runtime._defer中取出了程序計(jì)數(shù)器pc和棧指針sp并調(diào)用recovery函數(shù)觸發(fā)Goroutine,跳轉(zhuǎn)到recovery函數(shù)是通過runtime.call進(jìn)行的,我們看一下其源碼(src/runtime/asm_amd64.s 289行):
// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
TEXT runtime·mcall(SB), NOSPLIT, $0-8
MOVQ fn+0(FP), DI
get_tls(CX)
MOVQ g(CX), AX // save state in g->sched
MOVQ 0(SP), BX // caller's PC
MOVQ BX, (g_sched+gobuf_pc)(AX)
LEAQ fn+0(FP), BX // caller's SP
MOVQ BX, (g_sched+gobuf_sp)(AX)
MOVQ AX, (g_sched+gobuf_g)(AX)
MOVQ BP, (g_sched+gobuf_bp)(AX)
// switch to m->g0 & its stack, call fn
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ SI, AX // if g == m->g0 call badmcall
JNE 3(PC)
MOVQ $runtime·badmcall(SB), AX
JMP AX
MOVQ SI, g(CX) // g = m->g0
MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
PUSHQ AX
MOVQ DI, DX
MOVQ 0(DI), DI
CALL DI
POPQ AX
MOVQ $runtime·badmcall2(SB), AX
JMP AX
RET
因?yàn)?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">go語言中的runtime環(huán)境是有自己的堆棧和goroutine,recovery函數(shù)也是在runtime環(huán)境執(zhí)行的,所以要調(diào)度到m->g0來執(zhí)行recovery函數(shù),我們?cè)诳匆幌?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">recovery函數(shù):
// Unwind the stack after a deferred function calls recover
// after a panic. Then arrange to continue running as though
// the caller of the deferred function returned normally.
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")
}
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
在recovery 函數(shù)中,利用 g 中的兩個(gè)狀態(tài)碼回溯棧指針 sp 并恢復(fù)程序計(jì)數(shù)器 pc 到調(diào)度器中,并調(diào)用 gogo 重新調(diào)度 g ,將 g 恢復(fù)到調(diào)用 recover 函數(shù)的位置, goroutine 繼續(xù)執(zhí)行,recovery在調(diào)度過程中會(huì)將函數(shù)的返回值設(shè)置為1。這個(gè)有什么作用呢?在deferproc函數(shù)中找到了答案:
//go:nosplit
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.
}
當(dāng)延遲函數(shù)中recover了一個(gè)panic時(shí),就會(huì)返回1,當(dāng) runtime.deferproc 函數(shù)的返回值是 1 時(shí),編譯器生成的代碼會(huì)直接跳轉(zhuǎn)到調(diào)用方函數(shù)返回之前并執(zhí)行 runtime.deferreturn,跳轉(zhuǎn)到runtime.deferturn函數(shù)之后,程序就已經(jīng)從panic恢復(fù)了正常的邏輯。
第五部分,如果沒有遇到
runtime.gorecover就會(huì)依次遍歷所有的runtime._defer,在最后調(diào)用fatalpanic中止程序,并打印panic參數(shù)返回錯(cuò)誤碼2。
// fatalpanic implements an unrecoverable panic. It is like fatalthrow, except
// that if msgs != nil, fatalpanic also prints panic messages and decrements
// runningPanicDefers once main is blocked from exiting.
//
//go:nosplit
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
// Switch to the system stack to avoid any stack growth, which
// may make things worse if the runtime is in a bad state.
systemstack(func() {
if startpanic_m() && msgs != nil {
// There were panic messages and startpanic_m
// says it's okay to try to print them.
// startpanic_m set panicking, which will
// block main from exiting, so now OK to
// decrement runningPanicDefers.
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
// By crashing outside the above systemstack call, debuggers
// will not be confused when generating a backtrace.
// Function crash is marked nosplit to avoid stack growth.
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}
在這里runtime.fatalpanic實(shí)現(xiàn)了無法被恢復(fù)的程序崩潰,它在中止程序之前會(huì)通過 runtime.printpanics 打印出全部的 panic 消息以及調(diào)用時(shí)傳入的參數(shù)。
好啦,至此整個(gè)gopanic方法就全部看完了,接下來我們?cè)賮砜匆豢?code style="font-size: inherit;line-height: inherit;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin: 0px 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">gorecover方法。
5.3 gorecover
這個(gè)函數(shù)就簡(jiǎn)單很多了,代碼量比較少,先看一下代碼吧:
// The implementation of the predeclared function recover.
// Cannot split the stack because it needs to reliably
// find the stack segment of its caller.
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
首先獲取當(dāng)前所在的Goroutine,如果當(dāng)前Goroutine沒有調(diào)用panic,那么該函數(shù)會(huì)直接返回nil,是否能recover住該panic的判斷條件必須四個(gè)都吻合,p.Goexit判斷當(dāng)前是否是goexit觸發(fā)的,如果是則無法revocer住,上面講過會(huì)在gopanic中執(zhí)行進(jìn)行recover。argp是最頂層延遲函數(shù)調(diào)用的實(shí)參指針,與調(diào)用者的argp進(jìn)行比較,如果匹配說明調(diào)用者是可以recover,直接將recovered字段設(shè)置為true就可以了。這里主要的作用就是判斷當(dāng)前panic是否可以recover,具體的恢復(fù)邏輯還是由gopanic函數(shù)負(fù)責(zé)的。
5. 流程總結(jié)
上面看了一篇源碼,肯定也是一臉懵逼吧~。這正常,畢竟文字訴說,只能到這個(gè)程度了,還是要自己結(jié)合帶去去看,這里只是起一個(gè)輔助作用,最后做一個(gè)流程總結(jié)吧。
在程序執(zhí)行過程中如果遇到
panic,那么會(huì)調(diào)用runtime.gopanic,然后取當(dāng)前Goroutine的defer鏈表依次執(zhí)行。在調(diào)用
defer函數(shù)是如果有recover就會(huì)調(diào)用runtime.gorecover,在gorecover中會(huì)把runtime._panic中的recoved標(biāo)記為true,這里只是標(biāo)記的作用,恢復(fù)邏輯仍在runtime.panic中。在
gopanic中會(huì)執(zhí)行defer內(nèi)聯(lián)優(yōu)化、程序恢復(fù)邏輯。在程序恢復(fù)邏輯中,會(huì)進(jìn)行判斷,如果是觸發(fā)是runtime.Goexit,也會(huì)進(jìn)行recovery。panic也會(huì)進(jìn)行recovery,主要邏輯是runtime.gopanic會(huì)從runtime._defer結(jié)構(gòu)體中取出程序計(jì)數(shù)器pc和棧指針sp并調(diào)用runtime.recovery函數(shù)恢復(fù)程序。runtime.recvoery函數(shù)中會(huì)根據(jù)傳入的pc和sp在gogo中跳轉(zhuǎn)回runtime.deferproc,如果返回值為1,就會(huì)調(diào)用runtime.deferreturn恢復(fù)正常流程。在
gopanic執(zhí)行完所有的_defer并且也沒有遇到recover,那么就會(huì)執(zhí)行runtime.fatalpanic終止程序,并返回錯(cuò)誤碼2.
這就是這個(gè)邏輯流程,累死我了,如果文章對(duì)你有幫助,還請(qǐng)不吝點(diǎn)贊轉(zhuǎn)發(fā)~
-- END --
???
