本文主要跟大家介绍了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程序员掌握并大量使用。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。