乐趣区

关于go:28-GolangGo并发编程panic-defer-recover

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

panic/defer/recover 根本应用

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

package main

import "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 main

import "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 main

func main() {var data map[string]int

    data["test"] = 1
}

/*
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
        /test.go:6 +0x2e
*/

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

package main

import "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 main

import "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 3
defer 2
this is a panic
defer 1
test 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 main

import "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, AX
0x00aa 00170 (test.go:6)    JNE    288

0x0109 00265 (test.go:9)    CALL    runtime.deferreturn(SB)
0x010e 00270 (test.go:9)    MOVQ    192(SP), BP
0x0116 00278 (test.go:9)    ADDQ    $200, SP
0x011d 00285 (test.go:9)    RET
0x011e 00286 (test.go:9)    NOP
0x0120 00288 (test.go:6)    CALL    runtime.deferreturn(SB)
0x0125 00293 (test.go:6)    MOVQ    192(SP), BP
0x012d 00301 (test.go:6)    ADDQ    $200, SP
0x0134 00308 (test.go:6)    RET
*/

总结

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

退出移动版