翻译
https://blog.golang.org/defer-panic-and-recover
TLTR
- defer的函数在压栈的时候也会保存参数的值,而不是在执行时取值。换句话说,就是defer函数的参数在defer语句出现时,就已经确定下来了。
- 当外围函数执行完成之后,defer 函数是以后进先出的方式执行的
- defer 函数可以对函数的返回值进行读写
正文
Go有着常见的控制流:if, for, switch, goto。还有go这样的关键字来开启协程。这里我想讨论不那么常见的控制流:defer、panic和recover。
defer 语句将函数调用推送到list中。保存着这些函数调用的列表将会在调用defer语句的函数返回时执行。Defer常用于简化clean-up函数的调用。
译者注:可能用栈会更合适一些,defer语句的调用顺序是后进先出的。原文为list
译者注:clean-up函数,如流的关闭等
举个例子,让我们来看一个打开文件A、创建文件B、并将A文件的内容拷贝到文件B的函数
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的时候出错,打开的文件A没有关闭。通过在第9行添加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
语句的行为直接且可预测。有三条基本的规则
- defer的函数在压栈的时候也会保存参数的值,而不是在执行时取值。换句话说,就是defer函数的参数在defer语句出现时,就已经确定下来了。
在这个例子中,在defer PrintCall函数时,表达式i的值就被计算了。该语句会输出0,而不是1
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- 当外围函数执行完成之后,defer 函数是以后进先出的方式执行的
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
- defer 函数可以对函数的返回值进行读写
下面这个例子在包围的函数返回后,增加了i的值。这个函数会返回2
func c() (i int) {
defer func() { i++ }()
return 1
}
这样可以很方便地修改函数的错误返回值; 我们很快就会看到一个例子。
Panic 是一个用来停止原有的控制流的内置函数,然后开始panicking。 当函数F调用panic
之后,函数F的执行停止了,F中的任何defer函数都会执行,然后F返回给F的调用者。接下来对于调用者来说,调用程序F就像调用了panic一样。这个过程一直递归向上,直到当前goroutine的所有程序都返回。Panic可以通过直接调用panic来启动。也可以由运行时错误引起,如越界数组访问等。
译者注:在defer函数中的panic也会执行
Recover 是一个可以重新控制panicking的goroutine的函数。Recover仅能在defer函数中调用。在正常的执行流程中,对recover的调用会返回nil
并且不会造成任何影响。如果当前协程处于panicking的状态,recover的调用会捕获到这个值,并且继续正常的执行流
这是一个用来展示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调用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.
如果我们把defer
函数从f中移除,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包中也使用到了panic和recover。它使用一组递归函数对接口进行编码。 如果在遍历值时发生错误,则调用 panic 将堆栈unwind到顶级函数调用,该函数调用从 panic 中恢复并返回适当的错误值。
Go库中约定,即使包在内部使用 panic,其外部 API 仍会显示明确的错误返回值。
其他的defer例子:
defer中关闭锁:
mu.Lock()
defer mu.Unlock()
打印footer
printHeader()
defer printFooter()
等等
总而言之,defer为控制流提供了一种不寻常且强大的机制。 它可用于对由其他编程语言中的专用结构实现的许多功能进行建模。 现在就用起来吧!
译者注:就是说defer的功能十分强大。