剖析Go语言中的defer语句

【本文正在参加金石计划附加挑战赛------第二期命题】

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

  • 延迟执行:延迟语句会在包含它们的函数退出之前执行,无论函数是正常返回还是遇到异常。
  • 后进先出(LIFO):如果有多个延迟语句,它们将按照后进先出(LIFO)的顺序执行。也就是说,最后一个延迟语句最先执行,第一个延迟语句最后执行。

延迟语句通常用于以下情况:

  • 资源释放:延迟语句可用于在函数返回之前释放资源,如打开的文件、关闭数据库连接、释放锁等,以确保资源的正确释放,避免资源泄漏。
  • 错误处理:延迟语句可用于处理函数执行过程中可能发生的错误。通过在函数开头设置延迟语句,并在函数返回之前检查错误并进行相应处理,可以简化错误处理逻辑。
  • 日志记录:延迟语句可用于在函数返回之前进行日志记录或其他调试操作,以收集函数执行过程中的相关信息。

使用延迟语句可以提高代码的可读性和可维护性,同时确保资源释放和清理操作以相反的顺序执行。它是 Go 语言中处理资源管理和错误处理场景的常用技术。

在实际开发中,defer 的使用并不像上面描述的那么简单。如果使用不当,可能会导致难以察觉的错误。 我将从两个角度带你避免错误:

  • 首先,我们来剖析延迟语句的执行,注意 Go 中的 return 语句不是原子操作;
  • 其次,我将重点分享在 defer 语句后使用匿名函数和非匿名函数之间的区别。

剖析延迟语句

首先关键是理解以下语句:

go 复制代码
return xxx

编译后,上述语句实际上会生成三条指令:

  1. 将返回值赋给 xxx。
  2. 调用延迟函数。
  3. 执行空返回。

步骤 1 和 3 是 return 语句生成的指令,这意味着 return 不是原子指令;步骤 2 是 defer 定义的语句,可能会对返回值进行操作,从而影响最终结果。

我们来看两个例子,并尝试将 return 语句和 defer 语句分解为正确的顺序。

第一个例子

go 复制代码
func f() (r int) {
    t := 5

    defer func() {
       t = t + 5
    }()

    return t
}

分解后

go 复制代码
func f() (r int) {
    t := 5

    // 1. 赋值
    r = t

    // 2. 在赋值和空返回之间执行 defer
    func() {
       t = t + 5
    }()

    // 3. 空返回
    return
}

返回值将为 5。

第二个例子

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

    return 1
}

分解后

go 复制代码
func f() (r int) {
    // 1. 赋值
    r = 1

    // 2. 在赋值和空返回之间执行 defer 函数
    func(r int) {
       r = r + 5
    }(r)

    // 3. 空返回
    return
}

在第二步中,改变的是传入 r 的值,它是参数的副本,不会影响实际参数 r。因此,在主函数中调用f()将返回 1。

延迟匿名函数

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

  • 匿名函数作为 defer 参数:匿名函数可以直接在 defer 语句中定义,可以访问外部函数的变量,并且在执行时将使用当前变量值。这种方法允许在 defer 语句中方便地使用外部变量,但需要注意的是,变量值在执行时可能已经发生了变化。
  • 非匿名函数作为 defer 参数:非匿名函数需要事先定义,然后作为参数传递给 defer。在执行时,将使用函数的当前参数值。这种方法允许在 defer 语句中使用预定义函数,但需要注意函数参数的传递和作用域。

造成这种区别的原因在于匿名函数和非匿名函数在定义和作用域方面的不同。匿名函数可以直接在 defer 语句中定义并访问外部函数的变量,而非匿名函数需要事先定义,然后作为参数传递。这种设计灵活性允许开发人员根据具体需求选择使用 defer 语句的适当方法。

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

go 复制代码
package main

import "fmt"

func main() {
    x := 10

    defer func() {
       fmt.Println("延迟匿名函数:", x)
    }()

    x = 20
    fmt.Println("返回前:", x)
}

在上面的示例中,匿名函数作为 defer 的参数,允许它访问外部变量 x。 在函数返回之前,defer 语句中的匿名函数将执行并打印 x 的值。 输出结果如下:

go 复制代码
返回前:20
延迟匿名函数:20

当使用非匿名函数作为 defer 的参数时,需要事先定义函数,然后将函数名作为参数传递给 defer。 以下是示例代码:

go 复制代码
package main

import "fmt"

func main() {
    x := 10

    defer printX(x)

    x = 20
    fmt.Println("返回前:", x)
}

func printX(x int) {
    fmt.Println("延迟函数:", x)
}

在上面的示例中,函数printX作为 defer 的参数传递,该函数在主函数之后定义。 在函数返回之前,defer 语句中的printX函数将执行并打印传递给它的参数 x 的值。输出结果如下:

go 复制代码
返回前:20
延迟函数:10

总结

通过以上示例,我们可以清楚地看到使用匿名函数和非匿名函数作为 defer 参数之间的区别。 匿名函数可以直接在 defer 语句中定义并访问外部变量,而非匿名函数需要事先定义,然后作为参数传递。

相关推荐
uzong11 分钟前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天11 分钟前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享1 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪2 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
韩师傅2 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
栈与堆3 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥3 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似3 小时前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole