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

相关推荐
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
isolusion2 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
我是前端小学生3 小时前
Go语言中的方法和函数
go
TodoCoder3 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚4 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
Wyang_XXX4 小时前
CSS 选择器和优先级权重计算这么简单,你还没掌握?一篇文章让你轻松通关面试!(下)
面试
机器之心5 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴5 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven