在Go程序中defer特地常见,通常用来执行一些清理工作,须要留神defer先入后出个性(先申明的后执行);panic意味着一些出其不意的谬误产生,Go程序在panic异样退出的时候,会打印运行时栈不便排查问题;panic的谬误能够被recover捕捉,从而防止Go程序的退出,然而要留神recover只能在defer中,其余任何中央申明的recover是不能捕捉panic的。

panic/defer/recover根本应用

  Go程序的defer具备延后执行的能力,因而通常用来执行一些清理工作,例如文件的敞开,锁的开释等等。如上面事例所示:

package mainimport "sync"var lock = sync.Mutex{}func main() {    //1.承受到申请    //2.解决申请    doRequest()    //3.申请响应}//申请不能并行执行,所以须要加锁func doRequest() {    lock.Lock()    defer lock.Unlock()    //临界区代码逻辑    //这么执行也行,然而如果临界区呈现panic,锁将无奈开释    //lock.Unlock()}

  语句defer lock.Unlock()并没有立刻执行锁的开释操作,而是申明了一个延后执行操作,当doRequest函数返回时,会执行以后函数申明的defer操作,也就是doRequest函数返回时,才真正的开释锁。为什么要这么写呢?个别不是加锁,临界区代码,开释锁吗?想想如果临界区代码呈现panic呢?这时候还能执行锁的开释操作吗?而一旦锁没有胜利开释,后续其余申请不就全副阻塞了?

  这一点肯定要切记,针对一些资源的敞开,锁的开释等操作,肯定在defer执行,否则就有可能呈现死锁,资源泄露等等状况。

  咱们看到,doRequest执行结束返回时,才真正执行defer申明,那如果一个函数内申明了多个defer呢?函数返回时defer的执行程序是怎么样的呢?如上面的事例:

package mainimport "fmt"func main() {    for i := 0; i < 5; i ++ {        defer fmt.Println(i)    }}

  这段程序输入什么呢?其实这里波及两个问题:1)defer执行程序;2)defer传参问题。须要留神的是,在申明defer fmt.Println时,参数i作为fmt.Println函数的输出参数,值曾经明确了,且封装进interface数据类型,所以最终执行fmt.Println函数时,输入的是5个不同的值。另外,defer是先申明后执行的,所以最终执行程序应该反着来看,输入 4-3-2-1-0。

  panic意味着一些出其不意的谬误产生,Go程序在panic异样退出的时候,会打印运行时栈不便排查问题,例如map如果没有初始化,执行操作会panic;空指针援用也会panic;数组越界也会panic等等。如上面程序所示:

package mainfunc main() {    var data map[string]int    data["test"] = 1}/*panic: assignment to entry in nil mapgoroutine 1 [running]:main.main()        /test.go:6 +0x2e*/

  当然,咱们也能够通过panic函数手动抛出panic,留神Go程序在遇到panic时可是会异样退出的,个别为了防止程序退出,咱们会应用recover捕捉panic,只是须要记得recover只能在defer中。如上面程序所示:

package mainimport "fmt"func main() {    defer func() {        if rec := recover(); rec != nil {            //捕捉到panic,记录日志等            fmt.Println(rec)        }    }()    panic("this is a panic")}//this is a panic

  recover在Go程序作为HTTP服务时特地有用,总不能因为一个HTTP申请解决异样,导致整个服务退出吧?通常咱们在应用recover捕捉到panic时,会记录一些日志,包含运行时栈数据,以及HTTP申请,不便排查问题。

实现原理

  通过下面介绍,咱们根本理解了panic/defer/recover的根本应用,不过思考下,为什么defer是先申明后执行的呢?Go语言如何保障在函数返回时,执行以后函数内申明的defer呢?recover为什么只能在defer中呢?假如A协程抛出panic,在B协程能应用recover捕捉到吗?

  在深入研究panic/defer/recover实现原理之前,咱们先介绍下其对应的底层实现办法:

//参考文件runtime/panic.go// Create a new deferred function fn, which has no arguments and results.// The compiler turns a defer statement into a call to this.func deferproc(fn func())// deferprocStack queues a new deferred function with a defer record on the stack.func deferprocStack(d *_defer)// The implementation of the predeclared function panic.func gopanic(e any)// The implementation of the predeclared function recover.func gorecover(argp uintptr) any// deferreturn runs deferred functions for the caller's frame.// The compiler inserts a call to this at the end of any// function which calls defer.func deferreturn()

  看到deferreturn函数的正文,根本也就明确了Go语言如何保障在函数返回时执行以后函数内申明的defer。在编译阶段,如果检测到以后函数申明了defer,则会在函数开端增加deferreturn函数调用,该函数遍历以后函数申明的defer并执行:

func deferreturn() {    gp := getg()    for {        d := gp._defer        if d == nil {            return        }        //defer链表存储在以后协程G,d.sp=sp阐明defer就是在以后函数申明的        sp := getcallersp()        if d.sp != sp {            return        }                fn := d.fn        d.fn = nil        gp._defer = d.link        freedefer(d)        fn()    }}

  从deferreturn函数定义能够看到,defer链表是存储在以后协程G上的,所以在遍历过程中须要判断defer是否申明在以后函数,怎么判断呢?基于栈顶寄存器sp,在将defer退出到协程G链表时,记录了申明该defer时候的栈顶寄存器sp(也就是以后函数栈顶)。

  貌似从函数deferreturn的实现看不出来为什么defer是先申明后执行的,不过根本能确定协程G上保护了一个defer链表,那么在新增defer节点时,头插法是不是对应的就是栈呢?咱们简略看看deferproc函数的实现逻辑(deferprocStack相似):

func deferproc(fn func()) {    gp := getg()    d := newdefer()        //头插法    d.link = gp._defer    gp._defer = d    d.fn = fn    //设置函数栈顶寄存器sp以及指令寄存器pc    d.sp = getcallersp()    d.pc = getcallerpc()        return0()}

  还有一个问题,panic是怎么触发程序退出的呢?recover为什么只能在defer中呢?假如A协程抛出panic,在B协程能应用recover捕捉到吗?咱们先看看gopanic函数的实现逻辑:

func gopanic(e any) {    //遍历以后协程defer链表    for {        d := gp._defer        if d == nil {            break        }        //执行defer        d.fn()        pc := d.pc        sp := unsafe.Pointer(d.sp)        //recover捕捉,恢复程序执行        if p.recovered {            gp.sigcode0 = uintptr(sp)            gp.sigcode1 = pc            mcall(recovery)        }    }    //打印栈桢,exit(2)退出    fatalpanic(gp._panic) // should not return}

  在触发panic时,Go语言遍历以后协程defer链表,如果其中某个defer执行recover捕捉了异样,则恢复程序执行,否则最初通过exit(2)退出。看到这里根本也能猜出来,gorecover函数必定会设置p.recovered=true。另外,因为Go语言遍历的是以后协程defer链表,所以其余协程中defer+recover是无奈捕捉该panic的,而且如果recover不在defer中也是无奈捕捉的。

  最初一个问题,recover捕捉了panic,恢复程序执行后,下一条执行的指令是什么呢?其实能够直观的想想,当执行到某一个defer并且以后defer捕捉了异样,个别状况什么时候执行defer呢?函数执行结束返回之前!那假如某一个defer执行了,并且须要恢复程序失常执行流程,那怎么办?继续执行以后协程的defer显然不适合,这不是失常流程,只能依照以后defer所在函数执行完结返回的逻辑往下走了,也就是继续执行以后defer所在函数内申明的defer,如果没有,函数返回,返回到哪?当然是调用该函数的中央了!

package mainimport "fmt"func main() {    test()    fmt.Println("test end")}func test() {    defer fmt.Println("defer 1")    defer func() {        fmt.Println("defer 2")        if rec := recover(); rec != nil {            fmt.Println(rec)        }    }()    defer fmt.Println("defer 3")    panic("this is a panic")}/*defer 3defer 2this is a panicdefer 1test end*/

  仔细观察deferproc函数最初一行代码return0(),方才省略了其正文:

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

  如果deferproc返回1,跳转到函数返回处执行(deferreturn)。这怎么实现的呢?怎么返回1呢?deferproc不是在申明defer的时候就执行了吗?程序又是怎么跳转到这里而且还能返回1呢?defer内捕捉到panic后,通过mcall(recovery)复原了程序的执行(gopanic函数实现),就是这一行代码,跳转到了deferproc函数下一行代码,并且设置了返回值1

func recovery(gp *g) {    //联合gopanic + deferproc函数,这里的sp以及pc(就是调用deferproc函数时的寄存器地址)    sp := gp.sigcode0    pc := gp.sigcode1    gp.sched.sp = sp    gp.sched.pc = pc    //设置返回值为1    gp.sched.ret = 1    //跳转    gogo(&gp.sched)}

  recovery函数设置寄存器sp以及pc,以及返回值ret=1,跳转到该上下文持续执行程序。这里的pc就是调用deferproc函数时的寄存器地址,也就是deferproc下一行指令,就是这一行指令判断了返回值如果为1,跳转到函数开端执行deferreturn。当然这一行指令个别状况是看不到的,只能看汇编后的代码:

package mainimport "fmt"func main() {    defer fmt.Println(1)    fmt.Println("hello world")}//go tool compile -S -N -l test.go/*0x00a3 00163 (test.go:6)    CALL    runtime.deferprocStack(SB)0x00a8 00168 (test.go:6)    TESTL    AX, AX0x00aa 00170 (test.go:6)    JNE    2880x0109 00265 (test.go:9)    CALL    runtime.deferreturn(SB)0x010e 00270 (test.go:9)    MOVQ    192(SP), BP0x0116 00278 (test.go:9)    ADDQ    $200, SP0x011d 00285 (test.go:9)    RET0x011e 00286 (test.go:9)    NOP0x0120 00288 (test.go:6)    CALL    runtime.deferreturn(SB)0x0125 00293 (test.go:6)    MOVQ    192(SP), BP0x012d 00301 (test.go:6)    ADDQ    $200, SP0x0134 00308 (test.go:6)    RET*/

总结

  本篇文章次要介绍panic/defer/recover的根本应用以及实现原理,要切记针对一些资源的敞开,锁的开释等操作,肯定在defer执行,否则就有可能呈现死锁,资源泄露等等状况;另外,在程序可能呈现panic的中央,记得增加defer+recover,不然你的程序在遇到panic时可是会退出的。