Golang巧用defer进行错误处理的方法

发布时间 - 2026-01-11 01:15:08    点击率:

本文主要跟大家介绍了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程序员掌握并大量使用。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。


# golang  # defer  # error  # 深入理解Go语言中的闭包  # 举例讲解Go语言中函数的闭包使用  # Go基础教程系列之回调函数和闭包详解  # Go 中闭包的底层原理  # Go程序员踩过的defer坑错误处理  # Go语言中的函数、闭包、defer、错误处理的学习教程  # 重构  # 所示  # 多个  # 可以看出  # 时才  # 巧用  # 的是  # 是一个  # 中统  # 也不  # 都有  # 好了  # 带着  # 太多  # 相关内容  # 是有  # 却是  # 一句  # 就有  # 也很 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Laravel如何将应用部署到生产服务器_Laravel生产环境部署流程  Laravel如何与Pusher实现实时通信?(WebSocket示例)  ,南京靠谱的征婚网站?  Laravel如何处理JSON字段的查询和更新_Laravel JSON列操作与查询技巧  百度浏览器网页无法复制文字怎么办 百度浏览器复制修复  Edge浏览器怎么启用睡眠标签页_节省电脑内存占用优化技巧  BootStrap整体框架之基础布局组件  Midjourney怎样加参数调细节_Midjourney参数调整技巧【指南】  Laravel如何优雅地处理服务层_在Laravel中使用Service层和Repository层  如何制作公司的网站链接,公司想做一个网站,一般需要花多少钱?  Laravel怎么返回JSON格式数据_Laravel API资源Response响应格式化【技巧】  Laravel如何连接多个数据库_Laravel多数据库连接配置与切换教程  Laravel的.env文件有什么用_Laravel环境变量配置与管理详解  Windows驱动无法加载错误解决方法_驱动签名验证失败处理步骤  再谈Python中的字符串与字符编码(推荐)  Android中Textview和图片同行显示(文字超出用省略号,图片自动靠右边)  宙斯浏览器文件分类查看教程 快速筛选视频文档与图片方法  使用spring连接及操作mongodb3.0实例  Python文件异常处理策略_健壮性说明【指导】  详解Nginx + Tomcat 反向代理 如何在高效的在一台服务器部署多个站点  Gemini怎么用新功能实时问答_Gemini实时问答使用【步骤】  网站图片在线制作软件,怎么在图片上做链接?  Laravel怎么集成Vue.js_Laravel Mix配置Vue开发环境  高配服务器限时抢购:企业级配置与回收服务一站式优惠方案  如何实现javascript表单验证_正则表达式有哪些实用技巧  如何在IIS中新建站点并配置端口与物理路径?  Laravel Docker环境搭建教程_Laravel Sail使用指南  如何快速搭建安全的FTP站点?  如何在服务器上配置二级域名建站?  谷歌浏览器如何更改浏览器主题 Google Chrome主题设置教程  b2c电商网站制作流程,b2c水平综合的电商平台?  如何在阿里云域名上完成建站全流程?  Laravel策略(Policy)如何控制权限_Laravel Gates与Policies实现用户授权  Laravel如何实现全文搜索功能?(Scout和Algolia示例)  Microsoft Edge如何解决网页加载问题 Edge浏览器加载问题修复  SQL查询语句优化的实用方法总结  如何彻底卸载建站之星软件?  悟空识字怎么关闭自动续费_悟空识字取消会员自动扣费步骤  如何自定义safari浏览器工具栏?个性化设置safari浏览器界面教程【技巧】  如何快速搭建高效香港服务器网站?  Laravel观察者模式如何使用_Laravel Model Observer配置  宙斯浏览器怎么屏蔽图片浏览 节省手机流量使用设置方法  韩国网站服务器搭建指南:VPS选购、域名解析与DNS配置推荐  网站制作报价单模板图片,小松挖机官方网站报价?  北京网站制作的公司有哪些,北京白云观官方网站?  Laravel如何使用Blade组件和插槽?(Component代码示例)  Python正则表达式进阶教程_复杂匹配与分组替换解析  Laravel如何实现邮箱地址验证功能_Laravel邮件验证流程与配置  Laravel怎么调用外部API_Laravel Http Client客户端使用  Android实现代码画虚线边框背景效果