defer,panic 和 Recover

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

Golang的常用控制流程机制有:if,for,switch,goto,他还有go语句启动goroutine运行代码。在这里,我跟大家讨论一些不太常用的:defer,panic,recover

defer 语句会将一个函数添加到函数调用列表,等defer所在的函数返回时,会调用这个列表的所有函数(后进先出的方式)。延迟通常用于简化执行各种清理操作。

例如,让我们看一个打开两个文件并将其中一个文件的内容拷贝到另一个文件的函数:

func CopyFile(dstName, srcName string) (written uint64, 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失败,那么该函数会在没有关闭src文件就返回了。虽然这很容易通过在第二个return语句之前调用src.Close()来解决这个Bug,但是如果函数更复杂,则问题可能不会轻易被注意到并得到解决。通过使用defer语句,我们能够确保文件始终被关闭

func CopyFile(dstName, srcName string) (written uint64, 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语句被评估时,被defer语句修饰的函数的参数也会被评估。
    在此示例中,Println函数被defer修饰时就会计算表达式"i",函数返回后,延迟调用将会输出“0”
func a() {
    i := 0
    defer fmt.Prinln(i)
    i++
    return
}
  1. 在函数返回后,延迟调用函数按照后进先出的方式被调用.
    函数b将会输出3210
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  1. 延迟函数可以读取和修改函数的命名返回值
    此示例中,defer修饰的函数会在周围函数放回后会修改返回值。因此,函数返回"2"
func c() (i int) {
    defer func() {i++} ()
    return i
}

panic是一个内置函数,他能打断当前普通的控制留,并开始panicing。当函数F调用panic时,F的执行就会停止。F函数中在panic之前用defer语句声明的函数都将会正常执行。然后F返回给他的调用者。对于调用F函数的函数来说,F函数就表现的像panic函数一样。这个表现会一直向上抛,直到当前goroutine的所有函数都返回了,此时程序崩溃。Panics可由直接调用panic函数引起,也可以由运行时错误引起。例如:数据越界访问等。

recover是一个内置函数,可以控制goroutin的panic。recover仅在defer语句修饰的函数内有用。在正常的函数执行中,调用recover会返回nil并且没有其他任何效果。如果当前的goroutine正在发生panicing,在defer修饰的函数中调用recover能够捕获panic的值并且恢复正常执行。

下面是一个演示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)
}

这函数g有参数 int i,当i>3时触发panics,否则,传递参数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.

如果删除函数f中的defer修饰的函数。则此panic不会被捕获,并会等达到当前goroutine的调用堆栈顶部,终止程序。此修改后的程序输出如下:

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]

有关panic和recover的真实示例,请参考Go标准库里的json包,他使用一系列的递归函数对json编码的数据进行解码,当遇到错误的json格式时,解析器会调用panic将堆栈展开到顶级函数调用,该函数recover这panic并获得这个err值。

go库中约定,即使在包内使用panic时,其外部的API还是会显示明确的错误返回值。

defer的其他用户还有很多,包括释放锁:

mu.Lock()
defer mu.Unlock()

输出HtmlFooter

printHeader()
defer printFooter()

总之defer语句(包括使用或不实用panic和recover)提供了强大的控制流程的机制。他能模拟

其他语言中的专用结构实现的许多功能。你试试看。

By Andrew Gerrand

你可能感兴趣的:(defer,panic 和 Recover)