解密defer语句:避免踩坑,掌握延迟执行的正确姿势

2023年 11月 18日 60.2k 0

基本概念

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

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

  • 延迟执行:延迟语句会在包含它的函数执行结束前执行,无论函数是正常返回还是发生异常。
  • 后进先出:如果有多个延迟语句,它们会按照后进先出(LIFO)的顺序执行。也就是说,最后一个延迟语句会最先执行,而第一个延迟语句会最后执行。
  • 通常情况下,延迟语句在以下情况下使用:

  • 资源释放:延迟语句可以用于在函数返回前释放打开的文件、关闭数据库连接、释放锁等资源,以确保资源的正确释放,避免资源泄漏。
  • 错误处理:延迟语句可以用于处理函数执行过程中可能发生的错误。通过在函数开始时设置延迟语句,在函数返回前检查错误并进行相应的处理,可以简化错误处理的逻辑。
  • 日志记录:延迟语句可以用于在函数返回前记录日志或执行其他的调试操作,以便在函数执行过程中收集相关的信息。
  • 延迟语句的使用可以提高代码的可读性和可维护性,同时确保资源的释放和清理操作按照逆序进行。它是Go语言中一种常用的编程技巧,用于处理资源管理和错误处理等场景。

    避坑之旅

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

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

  • 首先拆解一下延迟语句的执行,注意Go语言的return语句不是原子性的;
  • 另外重点和大家分享一下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的参数时,主要区别在于对函数参数的传递和作用域的影响:

  • 匿名函数作为defer的参数:匿名函数可以直接在defer语句中定义,可以访问外部函数的变量,并且在执行时会使用当前的变量值。这种方式可以方便地在defer语句中使用外部变量,但需要注意变量的值在执行时可能已经发生了改变。
  • 非匿名函数作为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的语句的执行,相信大家可以更好的理解了。

    本文转载自微信公众号「 程序员升级打怪之旅」,作者「  王中阳Go」,可以通过以下二维码关注。

    转载本文请联系「 程序员升级打怪之旅」公众号。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论