Defer,Panic,and Recover
Defer,Panic,and Recover
Andrew Gerrand
4 August 2010
Go拥有一般的控制流程机制,像if、for、switch、goto。除此之外go也拥有一个单独的goroutine机制运行go语句。这里我想讨论一些不太常见的语法:defer,panic,and recover
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()失败,这个函数会返回一个没有关闭的文件资源。在第二个return语句调用之前放置一个src.Close()可以轻松的解决这个问题。但是通过引入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语句进行评价(使用)时,将对延迟函数的参数进行求值
在这个例子中,当Println()函数被延迟执行的时候,i运算式被使用,延迟调用将在函数返回后打印“0”。
func a() { i := 0 defer fmt.Println(i) i++ return }
2. 当周围的函数返回后,defer函数按照后进先出的顺序进行调用。
这个函数输出 “3210”
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
3. defer函数可以读取和分配给返回函数的命名返回值
这个例子中,defer函数在周围的函数执行后递增返回i,因此这个函数返回2
func c() (i int) { defer func() { i++ }() return 1 }
这对于修改错误返回值很方便,我们将很快看到这样的一个例子。
Panic是一个内置的函数,它可以停止常规控制流并开始panic,F函数调用了panic时,F的执行会被停止,F中的任何defer函数正常执行,然后F返回给它的调用者,对于调用者,F的行为是一个panic的调用,该过程将会继续向上进行堆栈直到返回当前的goroutine中的所有函数都返回,此时程序崩溃,panic可以直接通过引用panic来引发panic,它们也可以在程序运行错误的时候导致,比如越界数组的访问。
Recover是一个内置函数,它可以重新获取正在panic线程的控制。恢复仅仅在defer函数内部有用。当正常执行期间,recover会返回nil并且没有其它的效果。如果当前的goroutine正在panic,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,则发生panic,否则它将使用参数i+1进行调用自身,函数f defer会被调用reecover并打印恢复值(如果非零)的函数。再继续阅读之前,请尝试描绘出该程序的输出内容。
这个程序会输出
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 package它使用了一组递归函数对接口进行编码,如果遍历该值的时候发生了错误,则会调用panic将堆栈展开到顶级函数调用,该调用从panic中恢复并且返回适当的错误值(请参阅encode.go中encodeState类型的error和marshal方法)
在Go库中的约定甚至是当一个内部包使用了panic,它外部的API仍然会显示的显示出错误值。
defer的其他用法(在文件之外。前面给出的关闭示例)包括释放互斥量
mu.Lock() defer mu.Unlock()
打印尾部内容
printHeader() defer printFooter()
总而言之,defer语句(带有或不带有panic和recovery)提供了一种异常强大的控制流机制。它可以用来建模由其他编程语言中的专用结构实现的许多功能。试试看。