Defer,Panic,and Recover
最近面试碰到关于 defer的问题比较多,之前实际工作中并没有太深刻的理解。现在翻译一下 https://blog.golang.org/defer-panic-and-recover。这篇文章,加深对这几个内置关键字的理解。
前言
Go语言有常用饿控制流机制:if, for switch,goto.并且还有不同的机制跑在不同的goroutine中。这里主要讨论这几个相似的概念:defer panic recover。
derfer
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)// 可能创建失败 但是这时候os.Open已经调用成功
if err != nil {
// src.Close() 加在这里 保证句柄关闭
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
上述代码段可以工作,但是这里有个bug。如果在调用os.Create 的时候失败了,这时候函数就会返回,并且没有关闭源文件的句柄。虽然我们可以吧src.Close 放在第一个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的行为是可预测的,通过以下3个特性可以了解到。
- defer的函数变量在defer声明的时候就被确定下来。(考点)
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
这个例子里面,i初始化为0。到defer声明,这时候i还是0.进行函数入栈,相当于把0拷贝到函数中去。所以最后打印还是0. - 在当前函数执行完后defer函数链按照LIFO(后进先出)的原则调用。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
defer表达式会被放入一个类似于栈(stack)的结构,所以调用的顺序是后进先出的。。这个例子结合第一个特性,输出是3210。 - defer表达式中可以修改函数中的命名返回值
func c() (i int) {
defer func() { i++ }()
return 1
}
上面的示例程序,返回值变量名为i,在defer表达式中可以修改这个变量的值。所以,虽然在return的时候给返回值赋值为1,后来defer修改了这个值,让i自增了1,所以,函数的返回值是2而不是1。
堆栈处理
还有一种更加深入的理解
大意就是在defer出现的地方插入的指令
CALL runtime.deferproc
然后在函数返回之前的地方,插入指令
CALL runtime.deferreturn
go返回值的方式跟C是不一样的,为了支持多值返回,go是用栈返回值的,而C是用寄存器。return xxx 这一句语句并不是一条原子指令!
- 在没有defer之前 先把在栈中写一个值,这个值被会当作返回值。然后再调用RET指令返回。赋值指令 + RET指令
- 有了defer之后 赋值指令 + CALL defer指令 + RET指令
重点看这个例子:
//原本函数
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
//没有defer版本
func f() (r int) {
t := 5
r = t//进行拷贝
return // 直接返回
}
//有defer版本
func f() (r int) {
t := 5
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return //空的return指令
}
- 没有defer的版本 在return之前 由于具名返回值是r 所以先把t赋值给r。再做一个return操作。
- 有defer的版本 由于最终在栈上的是r。t和r是有两个不同内存地址的变量,所以t赋值给r之后,再修改t是不会改变r的值,并且也没有再次赋值。所以这里的返回值是5.
Panic
panic是一种可以停止普通控制流的内置功能。当函数F调用panic的时候,panic后面的函数停止执行,defer函数开始执行。对于函数F的调用者而言,就像它自身也调用了panic一样,继续向上抛出panic。直到当前的goroutine接受,导致进程崩溃。
- panic源于
-
- 我们直接调用panic函数
- 运行时引起的错误 除零异常 数组越界
Recover
recover是内置的可以收集对panic控制流的方法。recover方法只能够在defer中使用。在普通函数中调用recover会返回空置(nil)并没有别的效果。panic的时候recover不为空 通过这个判断,并且打出调用栈。如果当前的goroutine报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)
}
这个例子从函数f开始。
- recover部分
- g函数 回报 panic部分
- 递归调用 每一个增加defer函数
- i == 4 的时候触发painc 栈释放 这时候defer函数执行
- panic 被捕获, 并且恢复返回。
- 主函数部分f调用完毕,继续向下执行。
程序打印:
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.
关于panic应用实战
在官方库的json包中使用一组递归函数解码JSON编码的数据。当遇到格式错误的JSON时,解析器调用panic将堆栈展开到顶层函数调用,该函数调用从panic中恢复并返回适当的错误值。膜拜啊 这样子就减少了很多if err的判断。导致整条链路都是if err != nil.
参考: