写点什么

避免 defer 陷阱:拆解延迟语句,掌握正确使用方法

作者:王中阳Go
  • 2023-11-16
    北京
  • 本文字数:2296 字

    阅读完需:约 8 分钟

避免defer陷阱:拆解延迟语句,掌握正确使用方法

基本概念

Go 语言的延迟语句 defer 有哪些特点?通常在什么情况下使用?


Go 语言的延迟语句(defer statement)具有以下特点:


  1. 延迟执行:延迟语句会在包含它的函数执行结束前执行,无论函数是正常返回还是发生异常。

  2. 后进先出:如果有多个延迟语句,它们会按照后进先出(LIFO)的顺序执行。也就是说,最后一个延迟语句会最先执行,而第一个延迟语句会最后执行。


通常情况下,延迟语句在以下情况下使用:


  1. 资源释放:延迟语句可以用于在函数返回前释放打开的文件、关闭数据库连接、释放锁等资源,以确保资源的正确释放,避免资源泄漏。

  2. 错误处理:延迟语句可以用于处理函数执行过程中可能发生的错误。通过在函数开始时设置延迟语句,在函数返回前检查错误并进行相应的处理,可以简化错误处理的逻辑。

  3. 日志记录:延迟语句可以用于在函数返回前记录日志或执行其他的调试操作,以便在函数执行过程中收集相关的信息。


延迟语句的使用可以提高代码的可读性和可维护性,同时确保资源的释放和清理操作按照逆序进行。它是 Go 语言中一种常用的编程技巧,用于处理资源管理和错误处理等场景。

避坑之旅

实际开发中defer的使用并不像前面介绍的这么简单,defer用不好,会陷入泥潭。


下面我从两个角度带大家避坑:


  1. 首先拆解一下延迟语句的执行,注意 Go 语言的 return 语句不是原子性的;

  2. 另外重点和大家分享一下 defer 语句后面使用匿名函数和非匿名函数的区别。

拆解延迟语句

避免陷入泥潭的关键是必须深刻理解下面这条语句:


return xxx
复制代码


上面这条语句经过编译之后,实际上生成了三条指令:


1)返回值 =xxx。


2)调用 defer 函数。


3)空的 return。


第 1 和第 3 步是 return 语句生成的指令,也就是说 return 并不是一条原子指令;


第 2 步是 defer 定义的语句,这里可能会操作返回值,从而影响最终结果。


下面来看两个例子,试着将 return 语句和 defer 语句拆解到正确的顺序。

第一个例子:

func f()(r int){  t:=5
defer func(){ t=t+5 }() return t}
复制代码


拆解后:


func f()(r int){  t:=5    //1,赋值指令  r=t
// 2.defer 被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 func(){ t=t+5 }() //3.空的 return 指令 return }
复制代码


这里第二步实际上并没有操作返回值 r,因此,main 函数中调用 f()得到 5。


第二个例子:

func f()(r int){  defer func(r int){    r=r+5    }(r)        return 1}
复制代码


拆解后:


func f() (r int) {  //1.赋值   r=1    //2.这里改的r是之前传进去的r,不会改变要返回的那个r值   func(r int) {    r=r+5  }(r)    // 3. 空的 return   return}
复制代码


第二步,改变的是传值进去的 r,是形参的一个复制值,不会影响实参 r。因此,main 函数中需要调用 f()得到 1。


defer 匿名函数

在 Go 语言中,使用匿名函数作为 defer 的参数时,可以理解为:defer 语句中的匿名函数在包裹该 defer 语句的函数返回后才执行。这是因为 defer 语句的执行时机是在包裹函数即将返回之前,但在实际返回之前。


为什么不是在 return 语句之前执行呢?这是因为 defer 语句的设计初衷是为了在函数返回之前执行一些清理操作,例如关闭文件、释放资源等。将 defer 语句放在 return 语句之后,可以确保在函数返回之前执行这些清理操作,保证函数的执行完整性和资源的正确释放。


在使用匿名函数和非匿名函数作为 defer 的参数时,主要区别在于对函数参数的传递和作用域的影响:


  1. 匿名函数作为 defer 的参数:匿名函数可以直接在 defer 语句中定义,可以访问外部函数的变量,并且在执行时会使用当前的变量值。这种方式可以方便地在 defer 语句中使用外部变量,但需要注意变量的值在执行时可能已经发生了改变。

  2. 非匿名函数作为 defer 的参数:非匿名函数需要先定义好,然后作为 defer 的参数传递。在执行时,会使用函数的当前参数值。这种方式可以在 defer 语句中使用已定义的函数,但需要注意函数参数的传递和作用域。


产生这种区别的原因是,匿名函数和非匿名函数在定义和作用域上的差异。匿名函数可以直接在 defer 语句中定义,可以访问外部函数的变量,而非匿名函数需要先定义好,然后作为参数传递。这种设计灵活性使得开发者可以根据具体的需求选择合适的方式来使用 defer 语句。

举例来说

当使用匿名函数作为 defer 的参数时,可以在 defer 语句中直接定义匿名函数,并访问外部变量。


以下是一个示例代码:


package main
import "fmt"
func main() { x := 10
defer func() { fmt.Println("Deferred anonymous function:", x) }()
x = 20 fmt.Println("Before return:", x)}
复制代码


在上述示例中,匿名函数作为 defer 的参数,可以访问外部变量x。在函数返回之前,defer 语句中的匿名函数会执行,并打印出x的值。


输出结果如下:



当使用非匿名函数作为 defer 的参数时,需要先定义好函数,然后将函数名作为 defer 的参数传递。


以下是一个示例代码:


package main
import "fmt"
func main() { x := 10
defer printX(x)
x = 20 fmt.Println("Before return:", x)}
func printX(x int) { fmt.Println("Deferred function:", x)}
复制代码


在上述示例中,printX函数作为 defer 的参数传递,函数定义在 main 函数之后。


在函数返回之前,defer 语句中的printX函数会执行,并打印出传递的参数x的值。输出结果如下:


总结一下

通过以上示例,我们可以明确体现出使用匿名函数和非匿名函数作为 defer 的参数的区别。


匿名函数可以直接在 defer 语句中定义,并访问外部变量,而非匿名函数需要先定义好函数,然后将函数名作为参数传递。


通过前面带着大家拆解了 defer 的语句的执行,相信大家可以更好的理解了。


更多 defer 使用的技巧和踩坑经验,欢迎在评论区交流讨论。

欢迎加我微信:wangzhongyang1993

发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论 (1 条评论)

发布
用户头像
欢迎加我微信:wangzhongyang1993
刚刚 · 北京
回复
没有更多了
避免defer陷阱:拆解延迟语句,掌握正确使用方法_Go_王中阳Go_InfoQ写作社区