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

相关推荐
无忧无虑Coding3 小时前
pyinstall 打包Django程序
后端·python·django
求积分不加C4 小时前
Spring Boot中使用AOP和反射机制设计一个的幂等注解(两种持久化模式),简单易懂教程
java·spring boot·后端
枫叶_v4 小时前
【SpringBoot】26 实体映射工具(MapStruct)
java·spring boot·后端
2401_857617625 小时前
汽车资讯新趋势:Spring Boot技术解读
java·spring boot·后端
小林学习编程6 小时前
从零开始理解Spring Security的认证与授权
java·后端·spring
写bug的羊羊6 小时前
Spring Boot整合Nacos启动时 Failed to rename context [nacos] as [xxx]
java·spring boot·后端
2402_857589366 小时前
实验室管理效率提升:Spring Boot技术的力量
java·spring boot·后端
2401_857636397 小时前
Spring Boot图书馆管理系统:疫情中的技术实现
java·spring boot·后端
2401_857622667 小时前
实验室管理软件:Spring Boot技术构建
java·spring boot·后端
小林学习编程7 小时前
Springboot + vue 健身房管理系统项目部署
vue.js·spring boot·后端