go defer-recover-panic 学习

栏目: Go · 发布时间: 5年前

内容简介:本文将会讲解defer, recover,panic相关的知识。主要内容包括:其中重点在defer的原理。这部分包含了defer的定义、规则、实现原理、内部函数顺序四部分。希望看完本文,你能对defer、recover、panic有个全面的认识~

本文将会讲解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执行。

go defer-recover-panic 学习

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-recover-panic 学习》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

文明之光

文明之光

吴军 / 人民邮电出版社 / 2014-12 / 177元

吴军博士从对人类文明产生了重大影响却在过去被忽略的历史故事里,选择了有意思的几十个片段特写,以人文和科技、经济结合的视角,有机地展现了一幅人类文明发展的宏大画卷。 《文明之光》系列大致按照从地球诞生到近现代的顺序讲述了人类文明进程的各个阶段,每个章节相对独立,全景式地展现了人类文明发展历程中的多样性。《文明之光》系列首册讲述从人类文明开始到近代大航海这一历史阶段,共八个专题。第二册讲述了从近......一起来看看 《文明之光》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具