Go panic & recover

前言

之前針對于go 的錯誤和異常做了簡單的介紹百拓,對于panic介紹的不算多硝岗,本篇從原理和源碼的角度來看一下panic 和 recover是怎么運作的抡草。
panic 是一種不可預料的錯誤,會導致進程直接退出,跟c++ 中的core比較類似锅知,發(fā)生panic 會把發(fā)生問題時那個點的堆棧信息完整的打印到標準輸出中,然后崩潰退出脓钾。
在使用go時售睹,panic是非常危險的,即使你的程序有supervise之類的守護進程可训,不斷的掛掉重啟昌妹,也會嚴重的影響程序的可用性,通常來說我們使用recover來進行panic的捕獲握截,來阻止程序崩潰飞崖。

基礎使用

先來看一下demo:

func test() {
    defer func() {
        // do something
        fmt.println("c")
        if err:=recover();err!=nil{
            fmt.println("d")
            fmt.Println(err) // 這里的err其實就是panic傳入的內(nèi)容
        }
    }()
    fmt.println("a")
    // do something maybe panic
    panic("panic")
    fmt.println("b")
}

這里程序的輸出順序是:a\c\d\panic
panic 發(fā)生時,會直接從當前行跳出川蒙,如果有defer的recover將會被攔住蚜厉,執(zhí)行defer中的內(nèi)容。

通常來說畜眨,panic一般是由一些運行時錯誤導致的,比如說數(shù)組越界术瓮、空指針等康聂。針對這類問題:
1、寫代碼時要謹慎處理胞四,避免發(fā)生panic恬汁,
2、要有recover來阻止panic 崩潰程序辜伟。

原理

panic和recover關鍵字會在編譯時被編譯器轉換為OPANIC氓侧、ORECOVER類型的節(jié)點,然后進一步轉換成gopanic导狡、gorecover兩個運行時的函數(shù)調(diào)用约巷。
先來看一下panic的數(shù)據(jù)結構:
src/runtime/runtime2.go

//go:notinheap
type _panic struct {
    argp      unsafe.Pointer 
    arg       interface{}    
    link      *_panic        
    recovered bool           
    aborted   bool           
}

每次發(fā)生panic函數(shù)的調(diào)用時。都會創(chuàng)建上述結構體的一個實例來存儲相關的信息和結構旱捧。
其中:
argp 只想defer調(diào)用時參數(shù)的指針
argpanic的入?yún)?br> link指向更早調(diào)用的_panic的實例 (很顯然panic出現(xiàn)時是一個異常鏈)
recoveres表示當前是否被恢復(recover)
aborted是否被強行終止

panic 終止進程

沒有被recover的panic會導致程序直接退出独郎,主要在gopanic中做了這件事。
繼續(xù)看源碼:
src/runtime/runtime2.go l:445

func gopanic(e interface{}) {
    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")
    }

    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    atomic.Xadd(&runningPanicDefers, 1)

    for {
        d := gp._defer
        if d == nil {
            break
        }

        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
            }
            d._panic = nil
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
            continue
        }

        d.started = true
        
        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

        if gp._defer != d {
            throw("bad defer entry in panic")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link

        pc := d.pc
        sp := unsafe.Pointer(d.sp) 
        freedefer(d)
        if p.recovered {
            atomic.Xadd(&runningPanicDefers, -1)

            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") 
        }
    }

    preprintpanics(gp._panic)

    fatalpanic(gp._panic) 
    *(*int)(nil) = 0     
}

1枚赡、首先對內(nèi)部變量還有搶鎖的情況做了check氓癌。
2、獲取當前的goroutine
3贫橙、創(chuàng)建一個_panic實例
4贪婉、從當前的goroutine中獲取一個_defer結構體
5、如果_defer存在卢肃,調(diào)用reflectcall執(zhí)行_defer中的代碼
6疲迂、將下一個的_defer結構設置到 Goroutine 上并回到 4
7才顿、調(diào)用fatalpanic中止整個程序
其中,在fatalpanic中止整個程序之前就會通過printpanics打印出全部的panic消息以及調(diào)用時傳入的參數(shù)

func preprintpanics(p *_panic) {
    defer func() {
        if recover() != nil {
            throw("panic while printing panic value")
        }
    }()
    for p != nil {
        switch v := p.arg.(type) {
        case error:
            p.arg = v.Error()
        case stringer:
            p.arg = v.String()
        }
        p = p.link
    }
}

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")
}

fatalpanic會調(diào)用exit來退出程序鬼譬,并且返回錯誤碼2.

func fatalpanic(msgs *_panic) {
    pc := getcallerpc()
    sp := getcallersp()
    gp := getg()
    var docrash bool
    systemstack(func() {
        if startpanic_m() && msgs != nil {
            atomic.Xadd(&runningPanicDefers, -1)

            printpanics(msgs)
        }

        docrash = dopanic_m(gp, pc, sp)
    })

    if docrash {
        crash()
    }

    systemstack(func() {
        exit(2)
    })
    *(*int)(nil) = 0 // not reached
}

recover 恢復程序

上面介紹了panic崩潰程序的過程娜膘,接下來看一下recover阻止崩潰,恢復程序的過程优质。
看一下gorecover 函數(shù):

func gorecover(argp uintptr) interface{} {
    gp := getg()
    p := gp._panic
    if p != nil && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

這個函數(shù)非常簡單竣贪,修改panic結構體的recovered字段,當前函數(shù)的調(diào)用其實都發(fā)生在gopanic期間巩螃。
然后后期檢測這個字段的時候演怎,就不崩潰了(看一下gopanic函數(shù)就比較清晰了)

        if p.recovered {
            atomic.Xadd(&runningPanicDefers, -1)

            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") 
        }

從_defer結構體中取出了程序計數(shù)器pc和棧指針sp并調(diào)用recovery方法進行調(diào)度,調(diào)度之前會準備好sp避乏、pc以及函數(shù)的返回值爷耀。
這一塊兒就是panic和recover的過程啦。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拍皮,一起剝皮案震驚了整個濱河市歹叮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌铆帽,老刑警劉巖咆耿,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異爹橱,居然都是意外死亡萨螺,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門愧驱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慰技,“玉大人,你說我怎么就攤上這事组砚∥巧蹋” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵惫确,是天一觀的道長手报。 經(jīng)常有香客問我,道長改化,這世上最難降的妖魔是什么掩蛤? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮陈肛,結果婚禮上揍鸟,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好阳藻,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布晰奖。 她就那樣靜靜地躺著,像睡著了一般腥泥。 火紅的嫁衣襯著肌膚如雪匾南。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天蛔外,我揣著相機與錄音蛆楞,去河邊找鬼。 笑死夹厌,一個胖子當著我的面吹牛豹爹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播矛纹,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼臂聋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了或南?” 一聲冷哼從身側響起孩等,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎采够,沒想到半個月后瞎访,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡吁恍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了播演。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冀瓦。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖写烤,靈堂內(nèi)的尸體忽然破棺而出翼闽,到底是詐尸還是另有隱情,我是刑警寧澤洲炊,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布感局,位于F島的核電站,受9級特大地震影響暂衡,放射性物質(zhì)發(fā)生泄漏询微。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一狂巢、第九天 我趴在偏房一處隱蔽的房頂上張望撑毛。 院中可真熱鬧,春花似錦唧领、人聲如沸藻雌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胯杭。三九已至驯杜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間做个,已是汗流浹背鸽心。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叁温,地道東北人再悼。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像膝但,于是被迫代替她去往敵國和親冲九。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容