[翻译]defer,panic和recover

2020-05-13 技术笔记 1

go有一些常用的控制流语句:ifforswitchgoto。另外还有go关键词在不同的协程间调用代码。这篇文章讲解不太常用到的一个:defer,panicrecover
defer关键词将方法调用存在列表里。列表内的方法会在父方法返回结果时调用。defer通常用在一些方法内的清理操作。
例如,下面的方法打开两个文件,同时把一个内容复制到另一个文件内:

func CopyFile(dstName,srcNmae 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()方法调用失败,方法退出的时候源文件并没有正常的关闭。虽然可以在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的使用非常直接了当,但是他有三个简单的规则:

  1. defer定义的方法得变量在定义的时候就赋值了

下例中,i在输出的时候赋值是0,所以即使后面i++输出结果也没有改变

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  1. defer调用遵循后进先出的顺序执行

下面结果一次打印3210

func b() {
    for i:=0;i<4;i++ {
        defer fmt.Print(i)
    }
}
  1. defer方法会对返回结果进行读取赋值

下面例子中,defer方法在return之后读取并将变量i自增,所以方法返回结果为2

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

这对于确认方法的错误返回结果很方便。我们马上会看到另外一个例子。

panic是用控制程序控制流的内置方法。当方法F调用panicF的执行停止,F内的defer方法会正常调用,流程返回到调用F的位置。panic可直接调用,也可因运行错误(如数组越界)自动调用。

recover方法重新获取退出的控制流。它只在defer方法内有用。在正常执行过程中,recover会返回nil。程序崩溃时,recover会捕获到错误并执行正常操作。

下例是演示panicdefer如何操作

package main
import "fmt"
func main() {
    f()      
    fmt.Println("return 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("return normal 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方法定义了deferrecover,打印recover的返回结果。在看结果钱猜测下可能输出什么。
程序输出为:

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
return normally from f

如果把f函数中的defer去掉,程序输出结果则为

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
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]

panicrecover的实际用例,可以查看json.package标准库。定义了一系列递归方法的接口,如果遍历结果中发生错误,panic会释放到底层调用的堆栈,recover会返回正确的错误。(查看error和marshal方法编码状态在encode.go)
在go类库的惯例中,如果一个包内部使用了panic,外部调用的接口也会显式的展示返回结果。
defer的其他用法(除了关闭文件和之前的一些)也包含释放互斥锁

mu.Lock()
defer mu.Unlock()

打印footer:

printHeader()
defer printFooter()

等等

总的来说,defer(panic和recover)提供了一个非凡且功能强大的流程控制能力

原文链接

相关文章

快慢指针简单用法

快慢指针简单用法

如何避免愚蠢的见识

罗素关于如何避免有愚蠢的见识这种行为所需要的原则

Markdown 格式示例文章

Markdown 格式示例文章