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 :=
defer fmt.Println(i)
i++
return
}
例如该示例中,变量i会在defer时就被保存起来,所以defer函数执行时i的值是0.即便后面i的值变为了1,也不会影响之前的拷贝。
2.defer函数调用的顺序是后进先出。
func b() {
for i := ; i < ; i++ {
defer fmt.Print(i)
}
}
函数输出3210
3.defer函数可以读取和重新赋值函数的命名返回参数。
func c() (i int) {
defer func() { i++ }()
return
}
这个例子中,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()
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + )
}
函数g接受参数i,如果i大于3时触发panic,否则对i进行加1操作。函数f的defer函数里面调用了recover并且打印recover的值(非nil的话)。
程序将会输出:
Calling g.
Printing in g
Printing in g
Printing in g
Printing in g
Panicking!
Defer in g
Defer in g
Defer in g
Defer in g
Recovered in f
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 := ; i < ; i++ {
defer func() {
fmt.Println(i)
}()
}
程序将会输出:
因为闭包引用匿名函数外面的变量相当于是指针引用,得到的是变量的地址,实际到defer真正执行时,指针指向的内容已经发生的变化:

解决的方法:
for i := ; i < ; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
或者:
for i := ; i < ; i++ {
defer fmt.Println(i)
}
程序输出:
这里就不会用到闭包的上下文引用特性,是正经的函数参数拷贝传递,所以不会有问题。
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
Golang 高效实践之defer、panic、recover实践的更多相关文章
- Go语言异常处理defer\panic\recover
Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...
- defer,panic,recover
Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常,甚至一个小小的错误都抛出一个 ...
- defer, panic, recover使用总结
1. defer : 延迟调用.多个defer,依次入栈,在函数即将退出时,依次出栈调用 package main import "fmt" func main() { defer ...
- Go的异常处理 defer, panic, recover
Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...
- go语言defer panic recover用法总结
defer defer是go提供的一种资源处理的方式.defer的用法遵循3个原则 在defer表达式被运算的同时,defer函数的参数也会被运算.如下defer的表达式println运算的同时,其入 ...
- Golang 入门系列(十四)defer, panic和recover用法
以前讲过golang 的基本语法.但是,只是讲了一些基础的语法,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863 ...
- go panic recover 异常处理
Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱.因为开发者很容易滥用异常, ...
- Golang高效实践之泛谈篇
前言 我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如: <Golang高效实践之interface.reflection.json实践>&l ...
- Golang 高效实践之并发实践context篇
前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控 ...
随机推荐
- IIS 设置文件可下载
IIS遇到了文件不能下载的问题.为了让IIS能下载扩展名是INI的文件,我们需要如此操作:(括号内的是英文版本的名字) 打开“IIS管理器(InternetInformationServices(II ...
- CoolFormat(Qt Creator也可管理VC的Project)
http://download.csdn.net/download/akof1314/8457593 https://github.com/akof1314/CoolFormat http://dow ...
- 转载 《TypeScript 类型定义 DefinitelyTyped》
快速使用Romanysoft LAB的技术实现 HTML 开发Mac OS App,并销售到苹果应用商店中. <HTML开发Mac OS App 视频教程> 土豆网同步更新:http: ...
- Codility---FrogRiverOne
Task description A small frog wants to get to the other side of a river. The frog is initially locat ...
- Hadoop集群(第4期)VSFTP安装配置
1.VSFTP简介 VSFTP是一个基于GPL发布的类Unix系统上使用的FTP服务器软件,它的全称是Very Secure FTP 从此名称可以看出来,编制者的初衷是代码的安全. 安全性是编写VSF ...
- uni-app $refs的基本用法
$refs的基本用法 一个对象(Object),持有注册过 ref 特性 的所有 DOM 元素和组件实例. <template> <view class="containe ...
- SYN4104型 数字网同步时钟
SYN4104型 数字网同步时钟 产品概述 SYN4104型数字网同步时钟是由西安同步电子科技有限公司精心设计.自行研发生产的一款高精度锁相时钟频率源,接收GPS信号,使恒温晶振输出频率同步于GPS卫 ...
- 如何有效预防XSS?这几招管用!!!
原文链接 预防XSS,这几招管用 最近重温了一下「黑客帝国」系列电影,一攻一防实属精彩,生活中我们可能很少有机会触及那么深入的网络安全问题,但工作中请别忽略你身边的精彩 大家应该都听过 XSS (Cr ...
- 修改linux(kali)和windows双系统下默认启动系统和启动延时
我的公众号,正在建设中,欢迎关注: windows和kali双系统安装完成后kali是默认的启动系统,现将windows设置为默认启动系统并更改选择系统等待时间 1.开机时当运行到系统选择菜单时记下w ...
- docker部署asp.net core
上一篇文章我们成功的在win10上边安装了docker,这篇文章,我们将在docker中部署asp.net core程序, 先来一张运行成功的hello world镇楼 现在开始,首先创建一个asp. ...