<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          翻了源碼,我把 panic 與 recover 給徹底搞明白了

          共 30095字,需瀏覽 61分鐘

           ·

          2021-03-20 17:40

          點(diǎn)擊上方“Go編程時(shí)光”,選擇“加為星標(biāo)

          一時(shí)間關(guān)注Go技術(shù)干貨!




          0. 寫在前面

          今天與大家來聊一聊go語言中的"throw、try…..catch{}"。如果你之前是一名java程序員,我相信你一定吐槽過go語言錯(cuò)誤處理方式,但是這篇文章不是來討論好壞的,我們本文的重點(diǎn)是帶著大家看一看panicrecover是如何實(shí)現(xiàn)的。上一文我們講解了defer是如何實(shí)現(xiàn)的,但是沒有講解與defer緊密相連的recover,想搞懂panicrecover的實(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 ... catchrecover 可以中止 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 caseor 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([]byte1024)
           for {
            n := runtime.Stack(buf, false)
            if n < len(buf) {
             return buf[:n]
            }
            buf = make([]byte2*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)單的介紹了一下什么是panicrecover,下面我一起來看看他們有什么特性,避免我們踩坑。

          • 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
          panicpanic example3
                  panicpanic defer 1
                  panicpanic defer 2
          .......... 省略

          通過運(yùn)行結(jié)果可以看出panic不會(huì)影響defer函數(shù)的使用,所以他是安全的。

          • panic只會(huì)對(duì)當(dāng)前Goroutinedefer有效,還記得我們上一文分析的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è) Goroutinepanic 時(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è) Goroutinepanic 時(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)的匯編碼了,我們截取部分片段分析:

          img

          上面重點(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í)行:

          img

          這里最重點(diǎn)的就只有一個(gè),調(diào)用runtime.gorecover,也就是在這一步,對(duì)主程序中的panic進(jìn)行了恢復(fù)了,這就是panicrecover的執(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、spgoexit我們單獨(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是由先前的panicGoexit啟動(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, 0nil)
             }
            } 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.gorecoverrecoverd = 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ù)的返回值。

          第二部分主要是做panicrecover,這也與上面的流程基本差不多,他是從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)前Goroutinedefer鏈表依次執(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ù)傳入的 pcspgogo中跳轉(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 --


          喜歡明哥文章的同學(xué)
          歡迎長(zhǎng)按下圖訂閱!

          ???


          瀏覽 158
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丁香婷婷色五月激情综合 | 18禁在线亚洲 | 啪啪婷婷五月天 | 61无码 | MFYD-013 肉食人妻女上司が部下を誘惑し |