• Golang 高效实践之defer、panic、recover实践


     前言

    我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑。但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃。Golang有没有一种异常捕获和恢复机制呢?这个就是本文要讲的panic和recover。其中recover要配合defer使用才能发挥出效果。

    Defer

    Defer语句将一个函数放入一个列表(用栈表示其实更准确)中,该列表的函数在环绕defer的函数返回时会被执行。defer通常用于简化函数的各种各样清理动作,例如关闭文件,解锁等等的释放资源的动作。例如下面的这个函数打开两个文件,从一个文件拷贝内容到另外的一个文件:

    func CopyFile(dstName, srcName string) (written int64, err error) {
        src, err := os.Open(srcName)
        if err != nil {
            return
        }
    
        dst, err := os.Create(dstName)
        if err != nil {
            return
        }
    
        written, err = io.Copy(dst, src)
        dst.Close()
        src.Close()
        return
    }

    这段代码可以工作,但是有一个bug。如果调用os.Create失败,函数将会直接返回,并没有关闭srcName文件。修复的方法很简单,可以把src.Close的调用放在第二个return语句前面。但是当我们程序的分支比较多的时候,也就是说当该函数还有几个其他的return语句时,就需要在每个分支return前都要加上close动作。这样使得资源的清理非常繁琐而且容易遗漏。所以Golang引入了defer语句:

    func CopyFile(dstName, srcName string) (written int64, err error) {
        src, err := os.Open(srcName)
        if err != nil {
            return
        }
        defer src.Close()
    
        dst, err := os.Create(dstName)
        if err != nil {
            return
        }
        defer dst.Close()
    
        return io.Copy(dst, src)
    }

    在每个资源申请成功的后面都加上defer自动清理,不管该函数都多少个return,资源都会被正确的释放,例如上述例子的文件一定会被关闭。

    关闭defer语句,有三条简单的规则:

    1.defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。

    func a() {
        i := 0
        defer fmt.Println(i)
        i++
        return
    }

    例如该示例中,变量i会在defer时就被保存起来,所以defer函数执行时i的值是0.即便后面i的值变为了1,也不会影响之前的拷贝。

    2.defer函数调用的顺序是后进先出。

    func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }
    }

    函数输出3210

    3.defer函数可以读取和重新赋值函数的命名返回参数。

    func c() (i int) {
        defer func() { i++ }()
        return 1
    }

    这个例子中,defer函数中在函数返回时对命名返回值i进行了加1操作,因此函数返回值是2.可能你会有疑问,规则1不是说会在defer时保存i的值吗?保存的i是0,那加1操作之后也是1啊。这里就是闭包的魅力,i的值会被立马保存,但是保存的是i的引用,也可以理解为指针。当实际执行加1操作时,i的值其实被return置为了1,defer执行了加1操作i的值也就变成了2.

    Panic

    Panic是内建的停止控制流的函数。相当于其他编程语言的抛异常操作。当函数F调用了panic,F的执行会被停止,在F中panic前面定义的defer操作都会被执行,然后F函数返回。对于调用者来说,调用F的行为就像调用panic(如果F函数内部没有把panic recover掉)。如果都没有捕获该panic,相当于一层层panic,程序将会crash。panic可以直接调用,也可以是程序运行时错误导致,例如数组越界。

    Recover

    Recover是一个从panic恢复的内建函数。Recover只有在defer的函数里面才能发挥真正的作用。如果是正常的情况(没有发生panic),调用recover将会返回nil并且没有任何影响。如果当前的goroutine panic了,recover的调用将会捕获到panic的值,并且恢复正常执行。

    例如下面这个例子:

    package main
    
    import "fmt"
    
    func main() {
        f()
        fmt.Println("Returned normally from f.")
    }
    
    func f() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in f", r)
            }
        }()
        fmt.Println("Calling g.")
        g(0)
        fmt.Println("Returned normally from g.")
    }
    
    func g(i int) {
        if i > 3 {
            fmt.Println("Panicking!")
            panic(fmt.Sprintf("%v", i))
        }
        defer fmt.Println("Defer in g", i)
        fmt.Println("Printing in g", i)
        g(i + 1)
    }

    函数g接受参数i,如果i大于3时触发panic,否则对i进行加1操作。函数f的defer函数里面调用了recover并且打印recover的值(非nil的话)。

    程序将会输出:

    Calling g.
    Printing in g 0
    Printing in g 1
    Printing in g 2
    Printing in g 3
    Panicking!
    Defer in g 3
    Defer in g 2
    Defer in g 1
    Defer in g 0
    Recovered in f 4
    Returned normally from f.

    Panic和recover可以接受任何类型的值,因为定义为interface{}:

    func panic(v interface{})

    func recover() interface{}

    所以工作模式相当于:

    panic(value)->recover()->value

    传递给panic的value最终由recover捕获。

    另外defer可以配合锁的使用来确保锁的释放,例如:

    mu.Lock()

    Defer mu.Unlock()

    需要注意的是这样会延长锁的释放时间(需要等到函数return)。

    容易踩坑的一些例子

    通过上面的说明,我们已经对defer,panic和recover有了比较清晰的认识,下面通过一些实战中容易踩坑的例子来加深下印象。

    在循环里面使用defer

    不要在循环里面使用defer,除非你真的确定defer的工作流程,例如:

    只有当函数返回时defer的函数才会被执行,如果在for循环里面defer定义的函数会不断的压栈,可能会爆栈而导致程序异常。

    解决方法1:将defer移动到循环之外

    解决方法2:构造一层新的函数包裹defer

    defer方法

    没有指针的情况:

    type Car struct {
      model string
    }
    func (c Car) PrintModel() {
      fmt.Println(c.model)
    }
    func main() {
      c := Car{model: "DeLorean DMC-12"}
      defer c.PrintModel()
      c.model = "Chevrolet Impala"
    }

    程序输出DeLorean DMC-12。根据我们前面讲的内容,defer的时候会把函数和参考拷贝一份保存起来,所以c.model的值后面改变也不会影响defer的运行。

    有指针的情况:

    Car PrintModel()方法定义改为:

    func (c *Car) PrintModel() {
      fmt.Println(c.model)
    }

    程序将会输出Chevrolet Impala。这些defer虽然将函数和参数保存了起来,但是由于参数的值本身是针对,随意后面的改动会影响到defer函数的行为。

    同理的例子还有:

    for i := 0; i < 3; i++ {
      defer func() {
       fmt.Println(i)
      }()
    }

    程序将会输出:

    3
    3
    3

    因为闭包引用匿名函数外面的变量相当于是指针引用,得到的是变量的地址,实际到defer真正执行时,指针指向的内容已经发生的变化:

    解决的方法:

    for i := 0; i < 3; i++ {
      defer func(i int) {
       fmt.Println(i)
      }(i)
    }

    或者:

    for i := 0; i < 3; i++ {
      defer fmt.Println(i)
    }

    程序输出:

    2
    1
    0

    这里就不会用到闭包的上下文引用特性,是正经的函数参数拷贝传递,所以不会有问题。

    defer中修改函数error返回值

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func main() {
        {
            err := release()
            fmt.Println(err)
        }
    
        {
            err := correctRelease()
            fmt.Println(err)
        }
    }
    
    func release() error {
        defer func() error {
            return errors.New("error")
        }()
    
        return nil
    }
    
    func correctRelease() (err error) {
        defer func() {
            err = errors.New("error")
        }()
        return nil
    }

    release函数中error的值并不会被defer的return返回,因为匿名返回值在defer执行前就已经声明好并复制为nil。correctRelease函数能够修改返回值是因为闭包的特性,defer中的err是实际的返回值err地址引用,指向的是同一个变量。defer修改程序返回值error一般用在和recover搭配中,上述的情况属于滥用defer的一种情况,其实error函数值可以直接在程序的return中修改,不用defer。

    总结

    文章介绍了defer、panic和recover的原理和用法,并且在最后给出了一些在实际应用的实践建议,不要滥用defer,注意defer搭配闭包时的一些特性。

    参考

    https://blog.golang.org/defer-panic-and-recover

    https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01

    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa

    https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff

    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1

  • 相关阅读:
    Session的配置
    插件编程小窥
    今日遇到了困难,顺便看了下SMO
    初识三层架构
    .NET文件类库
    JQuery学习笔记
    反射整理学习<二>
    通俗道破单例模式
    菜鸟写代码生成器最后一天完结篇
    卧谈会:委托与事件
  • 原文地址:https://www.cnblogs.com/makelu/p/11226974.html
Copyright © 2020-2023  润新知