Golang 高效实践之defer、panic、recover实践
前言
我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑。但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃。Golang有没有一种异常捕获和恢复机制呢?这个就是本文要讲的panic和recover。其中recover要配合defer使用才能发挥出效果。
Defer
Defer语句将一个函数放入一个列表(用栈表示其实更准确)中,该列表的函数在环绕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失败,函数将会直接返回,并没有关闭srcName文件。修复的方法很简单,可以把src.Close的调用放在第二个return语句前面。但是当我们程序的分支比较多的时候,也就是说当该函数还有几个其他的return语句时,就需要在每个分支return前都要加上close动作。这样使得资源的清理非常繁琐而且容易遗漏。所以Golang引入了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自动清理,不管该函数都多少个return,资源都会被正确的释放,例如上述例子的文件一定会被关闭。
关闭defer语句,有三条简单的规则:
1.defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。
func a() { i := 0 defer fmt.Println(i) i++ return }
例如该示例中,变量i会在defer时就被保存起来,所以defer函数执行时i的值是0.即便后面i的值变为了1,也不会影响之前的拷贝。
2.defer函数调用的顺序是后进先出。
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
函数输出3210
3.defer函数可以读取和重新赋值函数的命名返回参数。
func c() (i int) { defer func() { i++ }() return 1 }
这个例子中,defer函数中在函数返回时对命名返回值i进行了加1操作,因此函数返回值是2.可能你会有疑问,规则1不是说会在defer时保存i的值吗?保存的i是0,那加1操作之后也是1啊。这里就是闭包的魅力,i的值会被立马保存,但是保存的是i的引用,也可以理解为指针。当实际执行加1操作时,i的值其实被return置为了1,defer执行了加1操作i的值也就变成了2.
Panic
Panic是内建的停止控制流的函数。相当于其他编程语言的抛异常操作。当函数F调用了panic,F的执行会被停止,在F中panic前面定义的defer操作都会被执行,然后F函数返回。对于调用者来说,调用F的行为就像调用panic(如果F函数内部没有把panic recover掉)。如果都没有捕获该panic,相当于一层层panic,程序将会crash。panic可以直接调用,也可以是程序运行时错误导致,例如数组越界。
Recover
Recover是一个从panic恢复的内建函数。Recover只有在defer的函数里面才能发挥真正的作用。如果是正常的情况(没有发生panic),调用recover将会返回nil并且没有任何影响。如果当前的goroutine panic了,recover的调用将会捕获到panic的值,并且恢复正常执行。
例如下面这个例子:
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接受参数i,如果i大于3时触发panic,否则对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.
Panic和recover可以接受任何类型的值,因为定义为interface{}:
func panic(v interface{})
func recover() interface{}
所以工作模式相当于:
panic(value)->recover()->value
传递给panic的value最终由recover捕获。
另外defer可以配合锁的使用来确保锁的释放,例如:
mu.Lock()
Defer mu.Unlock()
需要注意的是这样会延长锁的释放时间(需要等到函数return)。
容易踩坑的一些例子
通过上面的说明,我们已经对defer,panic和recover有了比较清晰的认识,下面通过一些实战中容易踩坑的例子来加深下印象。
在循环里面使用defer
不要在循环里面使用defer,除非你真的确定defer的工作流程,例如:
只有当函数返回时defer的函数才会被执行,如果在for循环里面defer定义的函数会不断的压栈,可能会爆栈而导致程序异常。
解决方法1:将defer移动到循环之外
解决方法2:构造一层新的函数包裹defer
defer方法
没有指针的情况:
type Car struct { model string } func (c Car) PrintModel() { fmt.Println(c.model) } func main() { c := Car{model: "DeLorean DMC-12"} defer c.PrintModel() c.model = "Chevrolet Impala" }
程序输出DeLorean DMC-12。根据我们前面讲的内容,defer的时候会把函数和参考拷贝一份保存起来,所以c.model的值后面改变也不会影响defer的运行。
有指针的情况:
Car PrintModel()方法定义改为:
func (c *Car) PrintModel() {
fmt.Println(c.model)
}
程序将会输出Chevrolet Impala。这些defer虽然将函数和参数保存了起来,但是由于参数的值本身是针对,随意后面的改动会影响到defer函数的行为。
同理的例子还有:
for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() }
程序将会输出:
3 3 3
因为闭包引用匿名函数外面的变量相当于是指针引用,得到的是变量的地址,实际到defer真正执行时,指针指向的内容已经发生的变化:
解决的方法:
for i := 0; i < 3; i++ { defer func(i int) { fmt.Println(i) }(i) }
或者:
for i := 0; i < 3; i++ { defer fmt.Println(i) }
程序输出:
2 1 0
这里就不会用到闭包的上下文引用特性,是正经的函数参数拷贝传递,所以不会有问题。
defer中修改函数error返回值
package main import ( "errors" "fmt" ) func main() { { err := release() fmt.Println(err) } { err := correctRelease() fmt.Println(err) } } func release() error { defer func() error { return errors.New("error") }() return nil } func correctRelease() (err error) { defer func() { err = errors.New("error") }() return nil }
release函数中error的值并不会被defer的return返回,因为匿名返回值在defer执行前就已经声明好并复制为nil。correctRelease函数能够修改返回值是因为闭包的特性,defer中的err是实际的返回值err地址引用,指向的是同一个变量。defer修改程序返回值error一般用在和recover搭配中,上述的情况属于滥用defer的一种情况,其实error函数值可以直接在程序的return中修改,不用defer。
总结
文章介绍了defer、panic和recover的原理和用法,并且在最后给出了一些在实际应用的实践建议,不要滥用defer,注意defer搭配闭包时的一些特性。
参考
https://blog.golang.org/defer-panic-and-recover
https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1