简单理解golang中的defer、panic和recover
代码测试环境
- MacOS:10.13.4
- Golang: version go1.10.1 darwin/amd64
- IDE:VSCode 1.24.0
写在前面
本文主要对Golang中的Defer、Panic、Revocer的用法进行了简单描述。
Defer
Defer语法会把跟在其后的函数调用保存到一个列表,主函数返回(return)后这个列表中保存的函数再依次调用。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
}
首先看一下上面用来复制文件的代码,主要分四个步骤:1)打开源文件,2)创建一个目标文件;3)把源文件的内容复制到目标文件;4)释放源文件和目标文件的资源。
上面的代码存在一个明显的资源泄露问题,比如dst, err := os.Create(dstName)
如果出现问题,那么src.Close()
将不能运行,资源便得不到释放。
如果有了defer,上面的问题就能很容易地解决了,比如下面的代码。通过使用Defer语句,能很优雅地把打开和释放资源的语句放在一起写,开发者不需再为释放资源而花费太多精力。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
// 打开一个文件立马就可以调用defer来关闭文件
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
// 打开一个文件立马就可以调用defer来关闭文件
defer dst.Close()
return io.Copy(dst, src)
}
使用Defer的几条规则
Defer使用起来很方便,不过有几条规则还是要注意:
- 调用defer时,被defer的函数的传入参数会在defer语句那里运算
比如下面的代码,函数返回后会输出“0”:
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- 在一个函数里可以多次调用defer语句,被defer的函数以后进先出的方式运行
比如下面的函数会输出“3210”:
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
- 在defer语法中调用的函数可以读取、赋值主函数被命名的返回值
如下面的函数,函数c会返回2:
func c() (i int) {
defer func() { i++ }()
return 1
}
通过查看golang的源码(比如encoding/json),defer的这个特性主要从来修改函数返回值中的error类型值。
Panic和Recover
Panic能够阻断正常的控制流,抛出异常,它是一个内建的函数。假如函数F调用了Panic,F会立即停止运行,立马运行Defer定义的函数,运行完以后F就返回给调用者了。异常理论上会一直向上传递,直到程序崩溃。
Recover是一个可以恢复控制流的内建函数,需要注意的是它只能在Defer函数内调用。假如函数没有抛出异常,Recover返回nil,否则,它能捕获Panic传过来的值,同时恢复控制流。
下面的例子描述了panic和defer的例子:
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)
}
输出为:
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的语句在函数的最后执行,比如g函数的Defer in g逆序输出现象;
- Panic异常会依次向调用链上游传递,比如在g函数抛出的异常(传递值为4)在f函数捕获到了;
- 如果异常被recover捕获,则不会再继续向上传递。比如未输出Returned normally from g.但是看到了Returned normally from f.,因为在f函数中调用g(0)时发生了异常;而在main函数调用f()函数时异常已经被f函数捕获,异常已经不存在了。
参考
- go语言中的defer、panic、recover处理异常 提供了例子,但是排版不太好看
- Defer, Panic, and Recover Golang官方博客的例子