剖析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 语句中定义并访问外部变量,而非匿名函数需要事先定义,然后作为参数传递。

相关推荐
天天扭码6 分钟前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
.生产的驴23 分钟前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
景天科技苑32 分钟前
【Rust】Rust中的枚举与模式匹配,原理解析与应用实战
开发语言·后端·rust·match·enum·枚举与模式匹配·rust枚举与模式匹配
追逐时光者1 小时前
MongoDB从入门到实战之Docker快速安装MongoDB
后端·mongodb
天天扭码1 小时前
深入讲解Javascript中的常用数组操作函数
前端·javascript·面试
方圆想当图灵1 小时前
深入理解 AOP:使用 AspectJ 实现对 Maven 依赖中 Jar 包类的织入
后端·maven
豌豆花下猫2 小时前
Python 潮流周刊#99:如何在生产环境中运行 Python?(摘要)
后端·python·ai
渭雨轻尘_学习计算机ing2 小时前
二叉树的最大宽度计算
算法·面试
嘻嘻嘻嘻嘻嘻ys2 小时前
《Spring Boot 3 + Java 17:响应式云原生架构深度实践与范式革新》
前端·后端
异常君2 小时前
线程池隐患解析:为何阿里巴巴拒绝 Executors
java·后端·代码规范