美文网首页
go defer-recover-panic 学习

go defer-recover-panic 学习

作者: 链人成长chainerup | 来源:发表于2019-06-04 08:10 被阅读0次

    本文将会讲解defer, recover,panic相关的知识。主要内容包括:

    • defer的原理
    • panic与recover的原理及注意事项

    其中重点在defer的原理。这部分包含了defer的定义、规则、实现原理、内部函数顺序四部分。

    希望看完本文,你能对defer、recover、panic有个全面的认识~

    一、defer的原理

    定义

    1、defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,主函数(创建defer的函数)返回前再把延迟的函数取出并执行。defer最常见的场景是完成一些收尾的工作,比如文件句柄的关闭等。还有就是执行 recover, 实现类似其他语言中的try catch finally。
    2、延迟函数可能有输入参数,这些参数可能来源于定义defer的函数,延迟函数也可能引用主函数用于返回的变量,也就是说延迟函数可能会影响主函数的一些行为。

    规则

    规则一、其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:
    返回值 = xxx
    调用defer的函数
    空的return
    
    规则一的几个案例

    下面通过一些案例进行总结:

    题目一
    func deferFuncParameter() {    
        var aInt = 1
        defer fmt.Println(aInt)
        aInt = 2
        return
    }
    

    延迟函数fmt.Println(aInt) 在defer语句出现时就已经确定了,所以无论后面如何修改aInt的值都不会影响延迟函数。
    上述程序转换之后是这样的:

    func deferFuncParameter() {    
        var aInt = 1
        anonymous = aInt // anonymous为匿名的变量
        aInt = 2
        fmt.Println(anonymous)
        return
    }
    

    即使是结构体,也是传值,也不会影响。比如

    type Test struct {
        value int
    }
    
    func (t Test) print() {
        println(t.value)
    }
    
    func main() {
        test := Test{}
        defer test.print()
        test.value += 1
    }
    

    这段代码输出的也是0.
    如果是结构体指针,则会影响输出。

    type Test struct {
        value int
    }
    
    func (t *Test) print() {
        println(t.value)
    }
    
    func main() {
        test := Test{}
        defer test.print()
        test.value += 1
    }
    

    这个输出的就是1, 因为传递的指针。

    题目二
    func printArray(array *[3]int) {    
       for i := range array {
           fmt.Println(array[i])
       }
    }
    
    func deferFuncParameter() {
       var aArray = [3]int{1, 2, 3}    
       defer printArray(&aArray)
       aArray[0] = 10
       return
    }
    
    func main() {
       deferFuncParameter()
    }
    

    函数deferFuncParameter定义了一个数组,通过defer调用printArray, 最后修改数组的第一个元素。printArray 函数接收数组的指针,即数组的地址,由于延迟函数执行时机在return语句之前,所以对数组的最终修改值被打印出来。

    题目三
    func deferFuncReturn() (result int) {    
        i := 1
        defer func() {
           result++
        }()    
        return i
    }
    

    函数的return语句并不是原子的,实际执行分为设置返回值->ret。defer语句实际执行在主函数返回(ret)前,即拥有defer的函数返回过程是 : 设置返回值->执行defer->ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回的是2.
    上述程序可以转换为

    func deferFuncReturn() (result int) {    
        i := 1
        result = i
        func() {
           result++
        }()    
        return 
    }
    

    总结上文的例子可以得出如下几个结论:

    • 延迟函数的参数在defer语句出现时就已经确定下来了
      如果是字面量,则肯定不受影响(如题目一); 如果是指针类型,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下defer后面的语句对变量的修改可能会影响延迟函数(如题目二)。
    • 延迟函数可能操作主函数的具名返回值
      关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
    规则二、从主函数返回值的角度看,有如下的几条规则:
    1、主函数拥有匿名返回值,返回字面值
    func foo() int {    
        var i int
        defer func() {
            i++
        }()    
        return 1
    }
    

    一个主函数拥有一个匿名的返回值,返回时使用字面值,比如”1“, ”hello“这样的值,这种情况下defer是无法操作返回值的。

    2、主函数拥有匿名返回值,返回变量

    一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。

    func foo() int {    
        var i int
        defer func() {
            i++
        }()    
        return i
    }
    

    上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:

    anony = i
    i++
    return
    

    由于i是整型,会将值拷贝给anony,所以defer语句修改i值,对函数返回值不会造成影响。

    3、主函数拥有具名返回值

    主函数声明语句中带有名字的返回值,会被初始化一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。

    func foo() (ret int) {    
        defer func() {
            ret++
        }()    
        return 0}
    

    上面的函数拆解之后是这样的:

    ret = 0
    ret++
    return
    

    defer实现原理

    数据结构

    每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。


    image.png
    defer的创建和执行

    源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。

    • deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
    • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

    可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

    defer内部函数顺序

    func TestDefer(t *testing.T) {
        fmt.Println("a")
        defer fmt.Println("b")
        defer c()
        defer d()
        fmt.Println("f")
    }
    
    func c() {
        fmt.Println("c")
    }
    
    func d() func(){
        fmt.Println("d")
        return func() {
            fmt.Println("e")
        }
    }
    

    输出为 a f d c b
    结论:defer函数在函数执行结束后执行,若有多个defer函数,则执行顺序为后进先出。 主函数中,defer d() 并没有执行d()返回的闭包,所以结果里面并没有返回e.

    func TestDefer(t *testing.T) {
        fmt.Println("a")
        defer fmt.Println("b")
        defer c()
        defer d()()
        fmt.Println("f")
    }
    
    func c() {
        fmt.Println("c")
    }
    
    func d() func(){
        fmt.Println("d")
        return func() {
            fmt.Println("e")
        }
    }
    

    这段代码只是在调用d方法时加了个括号,那么d方法返回的方法就会立即执行
    返回结果为 a d f e c b 。 为什么不是 a f d e c b 呢? 这是因为在defer d()() 编译时,首先定义了函数d(), 此时就输出了d. 然后返回包含e的闭包函数。
    即被defer标记的d函数中的程序“立即执行”,而d函数返回的函数则在测试方法结束后 按照“后进先出”的顺序执行。

    再看一个 来自effective go的例子:

    func trace(s string) string {
        fmt.Println("entering:", s)
        return s
    }
    
    func un(s string) {
        fmt.Println("leaving:", s)
    }
    
    func a() {
        defer un(trace("a"))
        fmt.Println("in a")
    }
    
    func b() {
        defer un(trace("b"))
        fmt.Println("in b")
        a()
    }
    
    func main() {
        b()
    }
    

    结果会打印

    entering: b
    in b
    entering: a
    in a
    leaving: a
    leaving: b
    

    没执行以前我以为是如下的结果:

    in b
    in a
    entring: a
    leaving: a
    entring: b
    leaving: b
    

    为什么不对呢?
    因为在编译时,b() 中的 defer un(trace("b")) ,是对un()函数的延迟,但是此时会执行trace("b")。
    这个例子说明,defer标记的函数只是最外层的函数,如果defer标记函数的参数也是个函数,则作为参数的函数在编译时就会被执行了,不必等到defer标记函数执行时才执行。

    二、panic与recover的原理及注意事项

    • panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获。
    • recover内置函数用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误如果goroutine中没有panic或者捕获的panic的值为nil,recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为

    几个注意的问题

    1、defer 表达式的函数如果定义在 panic 后面,该函数在 panic 后就无法被执行到
    func main() {
        panic("a")
        defer func() {
            fmt.Println("b")
        }()
    }
    

    结果 b没有打印出来
    而在defer后panic

    func main() {
        defer func() {
            fmt.Println("b")
        }()
        panic("a")
    }
    

    结果b被正常打印。

    2、F中出现panic时,F函数会立刻终止,不会执行F函数内panic后面的内容,但不会立刻return,而是调用F的defer,如果F的defer中有recover捕获,则F在执行完defer后正常返回,调用函数F的函数G继续正常执行
    func G() {
        defer func() {
            fmt.Println("c")
        }()
        F()
        fmt.Println("继续执行")
    }
    
    func F() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("捕获异常:", err)
            }
            fmt.Println("b")
        }()
        panic("a")
    }
    

    结果

    捕获异常: a
    b
    继续执行
    c
    
    3、如果F的defer中无recover捕获,则将panic抛到G中,G函数会立刻终止,不会执行G函数内后面的内容,但不会立刻return,而调用G的defer...以此类推
    func G() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("捕获异常:", err)
            }
            fmt.Println("c")
        }()
        F()
        fmt.Println("继续执行")
    }
    
    func F() {
        defer func() {
            fmt.Println("b")
        }()
        panic("a")
    }
    

    结果

    b
    捕获异常: a
    c
    
    4、如果一直没有recover,抛出的panic到当前goroutine最上层函数时,程序直接异常终止
    func G() {
        defer func() {
            fmt.Println("c")
        }()
        F()
        fmt.Println("继续执行")
    }
    
    func F() {
        defer func() {
            fmt.Println("b")
        }()
        panic("a")
    }
    

    结果

    b
    c
    panic: a
    
    goroutine 1 [running]:
    main.F()
        /xxxxx/src/xxx.go:61 +0x55
    main.G()
        /xxxxx/src/xxx.go:53 +0x42
    exit status 2
    
    5、recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成程序崩溃
    func G() {
        defer func() {
            //goroutine外进行recover
            if err := recover(); err != nil {
                fmt.Println("捕获异常:", err)
            }
            fmt.Println("c")
        }()
        //创建goroutine调用F函数
        go F()
        time.Sleep(time.Second)
    }
    
    func F() {
        defer func() {
            fmt.Println("b")
        }()
        //goroutine内部抛出panic
        panic("a")
    }
    

    结果:

    b
    panic: a
    
    goroutine 5 [running]:
    main.F()
        /xxxxx/src/xxx.go:67 +0x55
    created by main.main
        /xxxxx/src/xxx.go:58 +0x51
    exit status 2
    
    6、recover返回的是interface{}类型而不是go中的 error 类型,如果外层函数需要调用err.Error(),会编译错误,也可能会在执行时panic
    func main() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("捕获异常:", err.Error())
            }
        }()
        panic("a")
    }
    

    编译错误,结果

    err.Error undefined (type interface {} is interface with no methods)
    
    func main() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println("捕获异常:", fmt.Errorf("%v", err).Error())
            }
        }()
        panic("a")
    }
    

    结果:

    捕获异常: a
    

    参考文献

    Go defer实现原理剖析
    理解 Go 语言 defer 关键字的原理
    defer关键字
    golang中的defer函数的执行顺序
    go defer,panic,recover详解 go 的异常处理
    effective_go中文版
    谈谈 panic 和 recover 的原理,讲的比较深入

    相关文章

      网友评论

          本文标题:go defer-recover-panic 学习

          本文链接:https://www.haomeiwen.com/subject/kwbxxctx.html