Defer,Panic,and Recover

Defer,Panic,and Recover

最近面试碰到关于 defer的问题比较多,之前实际工作中并没有太深刻的理解。现在翻译一下 https://blog.golang.org/defer-panic-and-recover。这篇文章,加深对这几个内置关键字的理解。

前言

Go语言有常用饿控制流机制:if, for switch,goto.并且还有不同的机制跑在不同的goroutine中。这里主要讨论这几个相似的概念:defer panic recover。

derfer

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)// 可能创建失败 但是这时候os.Open已经调用成功
    if err != nil {
        // src.Close() 加在这里 保证句柄关闭
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

上述代码段可以工作,但是这里有个bug。如果在调用os.Create 的时候失败了,这时候函数就会返回,并且没有关闭源文件的句柄。虽然我们可以吧src.Close 放在第一个return之前,但是当函数更加复杂,我们可能就不那么轻易找到问题并且解决它了。通过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声明使得我们可以在打开句柄后正确的关闭。

特性

defer的行为是可预测的,通过以下3个特性可以了解到。

  • defer的函数变量在defer声明的时候就被确定下来。(考点)
    func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
    }
    这个例子里面,i初始化为0。到defer声明,这时候i还是0.进行函数入栈,相当于把0拷贝到函数中去。所以最后打印还是0.
  • 在当前函数执行完后defer函数链按照LIFO(后进先出)的原则调用。
    func b() {
    for i := 0; i < 4; i++ {
    defer fmt.Print(i)
    }
    }
    defer表达式会被放入一个类似于栈(stack)的结构,所以调用的顺序是后进先出的。。这个例子结合第一个特性,输出是3210。
  • defer表达式中可以修改函数中的命名返回值
    func c() (i int) {
    defer func() { i++ }()
    return 1
    }
    上面的示例程序,返回值变量名为i,在defer表达式中可以修改这个变量的值。所以,虽然在return的时候给返回值赋值为1,后来defer修改了这个值,让i自增了1,所以,函数的返回值是2而不是1。

堆栈处理

还有一种更加深入的理解

大意就是在defer出现的地方插入的指令

CALL runtime.deferproc

然后在函数返回之前的地方,插入指令

CALL runtime.deferreturn

go返回值的方式跟C是不一样的,为了支持多值返回,go是用栈返回值的,而C是用寄存器。return xxx 这一句语句并不是一条原子指令!

  • 在没有defer之前 先把在栈中写一个值,这个值被会当作返回值。然后再调用RET指令返回。赋值指令 + RET指令
  • 有了defer之后 赋值指令 + CALL defer指令 + RET指令

重点看这个例子:

//原本函数
func f() (r int) { 
    t := 5 
    defer func() { 
        t = t + 5 
    }() 
    return t
}

//没有defer版本
func f() (r int) { 
    t := 5 
    r = t//进行拷贝
    return // 直接返回
}

//有defer版本
func f() (r int) { 
    t := 5 
    r = t //赋值指令 
    func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 
        t = t + 5 
    } 
    return //空的return指令
}
  • 没有defer的版本 在return之前 由于具名返回值是r 所以先把t赋值给r。再做一个return操作。
  • 有defer的版本 由于最终在栈上的是r。t和r是有两个不同内存地址的变量,所以t赋值给r之后,再修改t是不会改变r的值,并且也没有再次赋值。所以这里的返回值是5.

Panic

panic是一种可以停止普通控制流的内置功能。当函数F调用panic的时候,panic后面的函数停止执行,defer函数开始执行。对于函数F的调用者而言,就像它自身也调用了panic一样,继续向上抛出panic。直到当前的goroutine接受,导致进程崩溃。

  • panic源于
    • 我们直接调用panic函数
    • 运行时引起的错误 除零异常 数组越界

Recover

recover是内置的可以收集对panic控制流的方法。recover方法只能够在defer中使用。在普通函数中调用recover会返回空置(nil)并没有别的效果。panic的时候recover不为空 通过这个判断,并且打出调用栈。如果当前的goroutine报panic,调用recover将会捕获这个值并且恢复异常。

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)
}

这个例子从函数f开始。

  • recover部分
  • g函数 回报 panic部分
    • 递归调用 每一个增加defer函数
    • i == 4 的时候触发painc 栈释放 这时候defer函数执行
  • panic 被捕获, 并且恢复返回。
  • 主函数部分f调用完毕,继续向下执行。

程序打印:

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应用实战

在官方库的json包中使用一组递归函数解码JSON编码的数据。当遇到格式错误的JSON时,解析器调用panic将堆栈展开到顶层函数调用,该函数调用从panic中恢复并返回适当的错误值。膜拜啊 这样子就减少了很多if err的判断。导致整条链路都是if err != nil.

参考:

https://studygolang.com/articles/742

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

猜你喜欢

转载自blog.csdn.net/u012279631/article/details/80621048