成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

深入理解 Go panic and recover

banana_pi / 586人閱讀

摘要:恢復(fù)流程如下判斷當(dāng)前中的是否已標(biāo)注為處理從鏈表中刪除已標(biāo)注中止的事件,也就是刪除已經(jīng)被恢復(fù)的事件將相關(guān)需要恢復(fù)的棧幀信息傳遞給方法的參數(shù)每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。

作為一個(gè) gophper,我相信你對(duì)于 panicrecover 肯定不陌生,但是你有沒有想過。當(dāng)我們執(zhí)行了這兩條語(yǔ)句之后。底層到底發(fā)生了什么事呢?前幾天和同事剛好聊到相關(guān)的話題,發(fā)現(xiàn)其實(shí)大家對(duì)這塊理解還是比較模糊的。希望這篇文章能夠從更深入的角度告訴你為什么,它到底做了什么事?

原文地址:深入理解 Go panic and recover

思考 一、為什么會(huì)中止運(yùn)行
func main() {
    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39
exit status 2

請(qǐng)思考一下,為什么執(zhí)行 panic 后會(huì)導(dǎo)致應(yīng)用程序運(yùn)行中止?(而不是單單說(shuō)執(zhí)行了 panic 所以就結(jié)束了這么含糊)

二、為什么不會(huì)中止運(yùn)行
func main() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("recover: %v", err)
        }
    }()

    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go 
2019/05/11 23:39:47 recover: EDDYCJY.

請(qǐng)思考一下,為什么加上 defer + recover 組合就可以保護(hù)應(yīng)用程序?

三、不設(shè)置 defer 行不

上面問題二是 defer + recover 組合,那我去掉 defer 是不是也可以呢?如下:

func main() {
    if err := recover(); err != nil {
        log.Printf("recover: %v", err)
    }

    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10 +0xa1
exit status 2

竟然不行,啊呀畢竟入門教程都寫的 defer + recover 組合 “萬(wàn)能” 捕獲。但是為什么呢。去掉 defer 后為什么就無(wú)法捕獲了?

請(qǐng)思考一下,為什么需要設(shè)置 deferrecover 才能起作用?

同時(shí)你還需要仔細(xì)想想,我們?cè)O(shè)置 defer + recover 組合后就能無(wú)憂無(wú)慮了嗎,各種 “亂” 寫了嗎?

四、為什么起個(gè) goroutine 就不行
func main() {
    go func() {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("recover: %v", err)
            }
        }()
    }()

    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go 
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14 +0x51
exit status 2

請(qǐng)思考一下,為什么新起了一個(gè) Goroutine 就無(wú)法捕獲到異常了?到底發(fā)生了什么事...

源碼

接下來(lái)我們將帶著上述 4+1 個(gè)小思考題,開始對(duì)源碼的剖析和分析,嘗試從閱讀源碼中找到思考題的答案和更多為什么

數(shù)據(jù)結(jié)構(gòu)
type _panic struct {
    argp      unsafe.Pointer
    arg       interface{} 
    link      *_panic 
    recovered bool
    aborted   bool 
}

panic 中是使用 _panic 作為其基礎(chǔ)單元的,每執(zhí)行一次 panic 語(yǔ)句,都會(huì)創(chuàng)建一個(gè) _panic。它包含了一些基礎(chǔ)的字段用于存儲(chǔ)當(dāng)前的 panic 調(diào)用情況,涉及的字段如下:

argp:指向 defer 延遲調(diào)用的參數(shù)的指針

arg:panic 的原因,也就是調(diào)用 panic 時(shí)傳入的參數(shù)

link:指向上一個(gè)調(diào)用的 _panic

recovered:panic 是否已經(jīng)被處理,也就是是否被 recover

aborted:panic 是否被中止

另外通過查看 link 字段,可得知其是一個(gè)鏈表的數(shù)據(jù)結(jié)構(gòu),如下圖:

恐慌 panic
func main() {
    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go
panic: EDDYCJY.

goroutine 1 [running]:
main.main()
    /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39
exit status 2

我們?nèi)シ床橐幌?panic 處理具體邏輯的地方在哪,如下:

$ go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x18
    0x0000 00000 (main.go:23)    TEXT    "".main(SB), ABIInternal, $24-0
    0x0000 00000 (main.go:23)    MOVQ    (TLS), CX
    0x0009 00009 (main.go:23)    CMPQ    SP, 16(CX)
    ...
    0x002f 00047 (main.go:24)    PCDATA    $2, $0
    0x002f 00047 (main.go:24)    MOVQ    AX, 8(SP)
    0x0034 00052 (main.go:24)    CALL    runtime.gopanic(SB)

顯然匯編代碼直指內(nèi)部實(shí)現(xiàn)是 runtime.gopanic,我們一起來(lái)看看這個(gè)方法做了什么事,如下(省略了部分):

func gopanic(e interface{}) {
    gp := getg()
    ...
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    
    for {
        d := gp._defer
        if d == nil {
            break
        }

        // defer...
        ...
        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

        // recover...
        if p.recovered {
            ...
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    preprintpanics(gp._panic)

    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

獲取指向當(dāng)前 Goroutine 的指針

初始化一個(gè) panic 的基本單位 _panic 用作后續(xù)的操作

獲取當(dāng)前 Goroutine 上掛載的 _defer(數(shù)據(jù)結(jié)構(gòu)也是鏈表)

若當(dāng)前存在 defer 調(diào)用,則調(diào)用 reflectcall 方法去執(zhí)行先前 defer 中延遲執(zhí)行的代碼,若在執(zhí)行過程中需要運(yùn)行 recover 將會(huì)調(diào)用 gorecover 方法

結(jié)束前,使用 preprintpanics 方法打印出所涉及的 panic 消息

最后調(diào)用 fatalpanic 中止應(yīng)用程序,實(shí)際是執(zhí)行 exit(2) 進(jìn)行最終退出行為的

通過對(duì)上述代碼的執(zhí)行分析,可得知 panic 方法實(shí)際上就是處理當(dāng)前 Goroutine(g) 上所掛載的 ._panic 鏈表(所以無(wú)法對(duì)其他 Goroutine 的異常事件響應(yīng)),然后對(duì)其所屬的 defer 鏈表和 recover 進(jìn)行檢測(cè)并處理,最后調(diào)用退出命令中止應(yīng)用程序

無(wú)法恢復(fù)的恐慌 fatalpanic
func fatalpanic(msgs *_panic) {
    pc := getcallerpc()
    sp := getcallersp()
    gp := getg()
    var docrash bool

    systemstack(func() {
        if startpanic_m() && msgs != nil {
            ...
            printpanics(msgs)
        }

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

    systemstack(func() {
        exit(2)
    })

    *(*int)(nil) = 0
}

我們看到在異常處理的最后會(huì)執(zhí)行該方法,似乎它承擔(dān)了所有收尾工作。實(shí)際呢,它是在最后對(duì)程序執(zhí)行 exit 指令來(lái)達(dá)到中止運(yùn)行的作用,但在結(jié)束前它會(huì)通過 printpanics 遞歸輸出所有的異常消息及參數(shù)。代碼如下:

func printpanics(p *_panic) {
    if p.link != nil {
        printpanics(p.link)
        print("	")
    }
    print("panic: ")
    printany(p.arg)
    if p.recovered {
        print(" [recovered]")
    }
    print("
")
}

所以不要以為所有的異常都能夠被 recover 到,實(shí)際上像 fatal errorruntime.throw 都是無(wú)法被 recover 到的,甚至是 oom 也是直接中止程序的,也有反手就給你來(lái)個(gè) exit(2) 教做人。因此在寫代碼時(shí)你應(yīng)該要相對(duì)注意些,“恐慌” 是存在無(wú)法恢復(fù)的場(chǎng)景的

恢復(fù) recover
func main() {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("recover: %v", err)
        }
    }()

    panic("EDDYCJY.")
}

輸出結(jié)果:

$ go run main.go 
2019/05/11 23:39:47 recover: EDDYCJY.

和預(yù)期一致,成功捕獲到了異常。但是 recover 是怎么恢復(fù) panic 的呢?再看看匯編代碼,如下:

$ go tool compile -S main.go
"".main STEXT size=110 args=0x0 locals=0x18
    0x0000 00000 (main.go:5)    TEXT    "".main(SB), ABIInternal, $24-0
    ...
    0x0024 00036 (main.go:6)    LEAQ    "".main.func1·f(SB), AX
    0x002b 00043 (main.go:6)    PCDATA    $2, $0
    0x002b 00043 (main.go:6)    MOVQ    AX, 8(SP)
    0x0030 00048 (main.go:6)    CALL    runtime.deferproc(SB)
    ...
    0x0050 00080 (main.go:12)    CALL    runtime.gopanic(SB)
    0x0055 00085 (main.go:12)    UNDEF
    0x0057 00087 (main.go:6)    XCHGL    AX, AX
    0x0058 00088 (main.go:6)    CALL    runtime.deferreturn(SB)
    ...
    0x0022 00034 (main.go:7)    MOVQ    AX, (SP)
    0x0026 00038 (main.go:7)    CALL    runtime.gorecover(SB)
    0x002b 00043 (main.go:7)    PCDATA    $2, $1
    0x002b 00043 (main.go:7)    MOVQ    16(SP), AX
    0x0030 00048 (main.go:7)    MOVQ    8(SP), CX
    ...
    0x0056 00086 (main.go:8)    LEAQ    go.string."recover: %v"(SB), AX
    ...
    0x0086 00134 (main.go:8)    CALL    log.Printf(SB)
    ...

通過分析底層調(diào)用,可得知主要是如下幾個(gè)方法:

runtime.deferproc

runtime.gopanic

runtime.deferreturn

runtime.gorecover

在上小節(jié)中,我們講述了簡(jiǎn)單的流程,gopanic 方法會(huì)調(diào)用當(dāng)前 Goroutine 下的 defer 鏈表,若 reflectcall 執(zhí)行中遇到 recover 就會(huì)調(diào)用 gorecover 進(jìn)行處理,該方法代碼如下:

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
}

這代碼,看上去挺簡(jiǎn)單的,核心就是修改 recovered 字段。該字段是用于標(biāo)識(shí)當(dāng)前 panic 是否已經(jīng)被 recover 處理。但是這和我們想象的并不一樣啊,程序是怎么從 panic 流轉(zhuǎn)回去的呢?是不是在核心方法里處理了呢?我們?cè)倏纯?gopanic 的代碼,如下:

func gopanic(e interface{}) {
    ...
    for {
        // defer...
        ...
        pc := d.pc
        sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
        freedefer(d)
        
        // recover...
        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") 
        }
    }
    ...
}

我們回到 gopanic 方法中再仔細(xì)看看,發(fā)現(xiàn)實(shí)際上是包含對(duì) recover 流轉(zhuǎn)的處理代碼的?;謴?fù)流程如下:

判斷當(dāng)前 _panic 中的 recover 是否已標(biāo)注為處理

_panic 鏈表中刪除已標(biāo)注中止的 panic 事件,也就是刪除已經(jīng)被恢復(fù)的 panic 事件

將相關(guān)需要恢復(fù)的棧幀信息傳遞給 recovery 方法的 gp 參數(shù)(每個(gè)棧幀對(duì)應(yīng)著一個(gè)未運(yùn)行完的函數(shù)。棧幀中保存了該函數(shù)的返回地址和局部變量)

執(zhí)行 recovery 進(jìn)行恢復(fù)動(dòng)作

從流程來(lái)看,最核心的是 recovery 方法。它承擔(dān)了異常流轉(zhuǎn)控制的職責(zé)。代碼如下:

func recovery(gp *g) {
    sp := gp.sigcode0
    pc := gp.sigcode1

    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), "]
")
        throw("bad recovery")
    }

    gp.sched.sp = sp
    gp.sched.pc = pc
    gp.sched.lr = 0
    gp.sched.ret = 1
    gogo(&gp.sched)
}

粗略一看,似乎就是很簡(jiǎn)單的設(shè)置了一些值?但實(shí)際上設(shè)置的是編譯器中偽寄存器的值,常常被用于維護(hù)上下文等。在這里我們需要結(jié)合 gopanic 方法一同觀察 recovery 方法。它所使用的棧指針 sp 和程序計(jì)數(shù)器 pc 是由當(dāng)前 defer 在調(diào)用流程中的 deferproc 傳遞下來(lái)的,因此實(shí)際上最后是通過 gogo 方法跳回了 deferproc 方法。另外我們注意到:

gp.sched.ret = 1

在底層中程序?qū)?gp.sched.ret 設(shè)置為了 1,也就是沒有實(shí)際調(diào)用 deferproc 方法,直接修改了其返回值。意味著默認(rèn)它已經(jīng)處理完成。直接轉(zhuǎn)移到 deferproc 方法的下一條指令去。至此為止,異常狀態(tài)的流轉(zhuǎn)控制就已經(jīng)結(jié)束了。接下來(lái)就是繼續(xù)走 defer 的流程了

為了驗(yàn)證這個(gè)想法,我們可以看一下核心的跳轉(zhuǎn)方法 gogo ,代碼如下:

// void gogo(Gobuf*)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB),NOSPLIT,$8-4
    MOVW    buf+0(FP), R1
    MOVW    gobuf_g(R1), R0
    BL    setg<>(SB)

    MOVW    gobuf_sp(R1), R13    // restore SP==R13
    MOVW    gobuf_lr(R1), LR
    MOVW    gobuf_ret(R1), R0
    MOVW    gobuf_ctxt(R1), R7
    MOVW    $0, R11
    MOVW    R11, gobuf_sp(R1)    // clear to help garbage collector
    MOVW    R11, gobuf_ret(R1)
    MOVW    R11, gobuf_lr(R1)
    MOVW    R11, gobuf_ctxt(R1)
    MOVW    gobuf_pc(R1), R11
    CMP    R11, R11 // set condition codes for == test, needed by stack split
    B    (R11)

通過查看代碼可得知其主要作用是從 Gobuf 恢復(fù)狀態(tài)。簡(jiǎn)單來(lái)講就是將寄存器的值修改為對(duì)應(yīng) Goroutine(g) 的值,而在文中講了很多次的 Gobuf,如下:

type gobuf struct {
    sp   uintptr
    pc   uintptr
    g    guintptr
    ctxt unsafe.Pointer
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr
}

講道理,其實(shí)它存儲(chǔ)的就是 Goroutine 切換上下文時(shí)所需要的一些東西

拓展
const(
    OPANIC       // panic(Left)
    ORECOVER     // recover()
    ...
)
...
func walkexpr(n *Node, init *Nodes) *Node {
    ...
    switch n.Op {
    default:
        Dump("walk", n)
        Fatalf("walkexpr: switch 1 unknown op %+S", n)

    case ONONAME, OINDREGSP, OEMPTY, OGETG:
    case OTYPE, ONAME, OLITERAL:
        ...
    case OPANIC:
        n = mkcall("gopanic", nil, init, n.Left)

    case ORECOVER:
        n = mkcall("gorecover", n.Type, init, nod(OADDR, nodfp, nil))
    ...
}

實(shí)際上在調(diào)用 panicrecover 關(guān)鍵字時(shí),是在編譯階段先轉(zhuǎn)換為相應(yīng)的 OPCODE 后,再由編譯器轉(zhuǎn)換為對(duì)應(yīng)的運(yùn)行時(shí)方法。并不是你所想像那樣一步到位,有興趣的小伙伴可以研究一下

總結(jié)

本文主要針對(duì) panicrecover 關(guān)鍵字進(jìn)行了深入源碼的剖析,而開頭的 4+1 個(gè)思考題,就是希望您能夠帶著疑問去學(xué)習(xí),達(dá)到事半功倍的功效

另外本文和 defer 有一定的關(guān)聯(lián)性,因此需要有一定的基礎(chǔ)知識(shí)。若剛剛看的時(shí)候這部分不理解,學(xué)習(xí)后可以再讀一遍加深印象

在最后,現(xiàn)在的你可以回答這幾個(gè)思考題了嗎?說(shuō)出來(lái)了才是真的懂 :)

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/31533.html

相關(guān)文章

  • 深入理解 Go defer

    摘要:在上一章節(jié)深入理解中,我們發(fā)現(xiàn)了與其關(guān)聯(lián)性極大,還是覺得非常有必要深入一下。而返回的就是,因此可以防止重復(fù)調(diào)用小結(jié)在這個(gè)函數(shù)中會(huì)為新的設(shè)置一些基礎(chǔ)屬性,并將調(diào)用函數(shù)的參數(shù)集傳入。 在上一章節(jié) 《深入理解 Go panic and recover》 中,我們發(fā)現(xiàn)了 defer 與其關(guān)聯(lián)性極大,還是覺得非常有必要深入一下。希望通過本章節(jié)大家可以對(duì) defer 關(guān)鍵字有一個(gè)深刻的理解,那么我...

    Developer 評(píng)論0 收藏0
  • Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器

    摘要:還有一種情況就是當(dāng)你在一行中寫了多個(gè)語(yǔ)句,也需要使用分號(hào)來(lái)分開由于語(yǔ)言詞法分析器添加分號(hào)的特殊性,所以在有些情況下需要注意你都不應(yīng)該將一個(gè)控制結(jié)構(gòu)或的左大括號(hào)放在下一行。 Go語(yǔ)言中變量的聲明和JavaScript很像,使用var關(guān)鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個(gè)變量 var m int = 10 // 聲明初始化多個(gè)變量 var i, j...

    simon_chen 評(píng)論0 收藏0
  • Go語(yǔ)言的變量、函數(shù)、Socks5代理服務(wù)器

    摘要:還有一種情況就是當(dāng)你在一行中寫了多個(gè)語(yǔ)句,也需要使用分號(hào)來(lái)分開由于語(yǔ)言詞法分析器添加分號(hào)的特殊性,所以在有些情況下需要注意你都不應(yīng)該將一個(gè)控制結(jié)構(gòu)或的左大括號(hào)放在下一行。 Go語(yǔ)言中變量的聲明和JavaScript很像,使用var關(guān)鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個(gè)變量 var m int = 10 // 聲明初始化多個(gè)變量 var i, j...

    Ajian 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<