Golang巧用defer进行错误处理的方法
作者:_张晓龙_ 发布时间:2023-08-05 03:21:13
本文主要跟大家介绍了Golang巧用defer进行错误处理的相关内容,分享出来供大家参考学习,下面来看看详细的介绍:
问题引入
毫无疑问,错误处理是程序的重要组成部分,有效且优雅的处理错误是大多数程序员的追求。很多程序员都有C/C++的编程背景,Golang的程序员也不例外,他们处理错误有意无意的带着C/C++的烙印。
我们看看下面的例子,就有一种似曾相识的赶脚,代码如下:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
err = createResource2()
if err != nil {
destroyResource1()
return ERR_CREATE_RESOURCE2_FAILED
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ERR_CREATE_RESOURCE3_FAILED
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
从代码的实现中可以看出:在一个函数中,当创建新资源失败时,则要清理所有前面已经创建成功的资源,这使得函数中有了重复代码的坏味道,比如destroyResource1函数调用了3次,destroyResource2函数调用了2次。
重构一:一个defer + 多个flag
Golang提供了一个很好用的关键字defer,当包含defer的函数执行完毕时(不管是通过return的正常结束,还是由于panic导致的异常结束),defer语句才被调用。
考虑到这一点,我们尝试将所有资源在defer语句中统一清理。由于函数返回时,不知道是否需要清理以及清理那些资源,所以要增加多个flag。
重构后的代码如下所示:
func deferDemo() error {
flag := false
flag1 := false
flag2 := false
flag3 := false
defer func() {
if !flag {
if flag3 {
destroyResource3()
}
if flag2 {
destroyResource2()
}
if flag1 {
destroyResource1()
}
}
}()
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
flag1 = true
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
flag2 = true
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
flag3 = true
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
flag = true
return nil
}
从重构后的代码可以看出,虽然消除了重复,但是引入了太多的flag:
flag表示函数是否执行成功,即flag为true时表示函数执行成功,否则表示函数执行失败;在defer语句中,只有flag为false时才需要统一清理资源
flagi表示第i个资源是否创建成功,即flagi为true时表示第i个资源创建成功,否则表示第i个资源创建失败;在defer语句中,只有flagi为true时才需要清理第i个资源
显然,这不是我们想要的
重构二:多个defer
看过linux源码的同学都知道,在内核代码中,很多地方都通过goto语句来集中处理错误,非常优雅。
我们用这种方法将重构前的代码用C语言写一下,代码如下所示:
ErrCode deferDemo()
{
ErrCode err = createResource1();
if (err != ERR_SUCC)
{
goto err_1;
}
err = createResource2();
if (err != ERR_SUCC)
{
goto err_2;
}
err = createResource3();
if (err != ERR_SUCC)
{
goto err_3;
}
err = createResource4();
if (err != ERR_SUCC)
{
goto err_4;
}
return ERR_SUCC;
err_4:
destroyResource3();
err_3:
destroyResource2();
err_2:
destroyResource1();
err_1:
return ERR_FAIL;
}
没有重复,没有flag,错误处理也很优雅,感觉很爽,那以前在C/C++编码规范中禁止使用goto语句的规则确实有点过,呵呵...
从重构后的C代码中可以看出,create操作和destroy操作的顺序类似入栈和出栈的顺序:
伴随着create操作,destroy操作逐个入栈,顺序为1,2,3
出栈时是destroy操作,顺序为3,2,1
于是我们又想到了defer语句:当Golang的代码执行时,如果遇到defer语句,则压入堆栈,当函数返回时,会按照后进先出的顺序调用defer语句。
我们看一个例子,代码如下所示:
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
}
运行后,日志如下所示:
3
2
1
然而,有堆栈特性还不够,因为伴随着create操作,destroy操作入栈是有条件的:
如果create操作失败,则直接返回,那么defer语句没有执行,导致destroy操作没有入栈
如果create操作成功,则defer语句得到执行,destroy操作完成入栈
可见,destroy操作的入栈条件是create操作成功,但是destroy操作并不是一定执行,只有当某个create操作失败("err != nil")时,前面入栈的destory操作才需要执行,所以err的值也需要入栈。然而,destroy操作入栈时"err == nil" ,于是问题就变成:当err的值在后面变成非nil时,应该同步修改堆栈中的err值,即堆栈中传递的是引用或指针而不是值。
当err的引用或指针和destroy操作都需要入栈时,defer后面必须是一个闭包调用。我们知道,对于闭包的参数是值传递,而对于外部变量却是引用传递。为了简单优雅起见,我们将err不通过参数的指针传递,而通过外部变量的引用传递。
我们根据这个结论重构一下代码,如下所示:
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}
本次重构消除了代码的坏味道,不由的感叹一句:”升级了,我的哥!“
总结
本文通过巧用defer,有效且优雅的处理了错误,该技巧应该被所有的Golang程序员掌握并大量使用。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助
来源:http://www.jianshu.com/p/de61abdcee8d


猜你喜欢
- 完整项目地址下载:https://github.com/rainbow-tan/rainbow/tree/master/%E8%A3%81%
- mysql的case when字段为空,nullname字段为null时替换为 ‘该字段为空’SEL
- 本文实例讲述了python追加元素到列表的方法。分享给大家供大家参考。具体实现方法如下:scores = ["1",&q
- 1.使用jobsName.ini文件保存要创建job的名字jobs1jobs2jobs32.使用Jenkins创建job时自动生成的conf
- 介绍我们一起来做个示例,在.NET中新建一个类,并在这个类里新建一个方法,然后在SQL Server中调用这个方法。按照微软所述,通过宿主
- 最近突然发现我们部署在数据库上面的告警(Alert),当错误日志里面出现错误时,并不是每个错误日志都会发送邮件出来。如下所示,设置了告警“S
- 废话不多说啦,直接看代码吧!tf.concatt1 = [[1, 2, 3], [4, 5, 6]]t2 = [[7, 8, 9], [10
- 接触编程的朋友都听过正则表达式,在python中叫re模块,属于文字处理服务里面的一个模块。re里面有一个方法叫match,接下来的文章我来
- 问题1问题描述:TypeError: default_collate: batch must contain tensors, numpy
- 检测自己当前系统环境中python是否已经安装该module,若未安装请自行安装检测自己的pycharm使用的环境变量是否与当前环境一致若不
- 我试了网上提供的一些方法都不行,最后还是自己用SQL解决了些问题。 1 在查询分析器里面选中出问题的数据库,然后输入: Exec sp_co
- 本文实例为大家分享了python读取Excel实例的具体代码,供大家参考,具体内容如下1.操作步骤:(1)安装python官方Excel库-
- datetime 和 smalldatetime 代表日期和一天内的时间的日期和时间数据类型。 Microsoft SQL Server 用
- 最近做某项目的数据库分析,要实现对海量数据的导入问题,就是最多把200万条数据一次导入sqlserver中,如果使用普通的insert语句进
- 今日一同时问我,new Date(Date(str))这段代码什么意思?我一看就晕了,一个new Date 一个Date这是什么意思?这函数
- ORACLE的这类错误在ORALCE的文档中有详细说明,但原因及措施说明不详细,本文当着重说明如何解决这类错误。1、ORA-12571、OR
- 不话不多说了,直接上代码吧:<div class="layui-btn-group demoTable" styl
- 我们在上传大文件时,可能会由于服务器的原因导致文件上传失败,文件过大时由于服务器的配置或响应事件过长导致上传文件失败,这时候我们可以将一个大
- 问题背景在开始正文之前,感谢用户名为怜索的朋友送给了我的博客2021年的第一个赞!import numpy as npimport matp
- python opencv实现目标跟踪python-opencv3.0新增了一些比较有用的 * 算法这里根据官网示例写了一个 * 类程序只能