Go的defer语句

什么是 defer 语句

编程的时候,经常需要申请一些资源,比如数据库连接、文件、锁等,这些资源需要在用完之后释放掉,否则会造成内存泄漏。但开发人员经常容易忘记释放这些资源,从而造成一些事故。Go 语言直接在语言曾谜案提供 defer 关键字,在申请资源语句的下一行,可以直接用 defer 语句来注册函数结束后执行释放资源的动作。

defer 语句采用后进先出的设计,类似于栈的方式,函数执行时,每遇到一个 defer 都会把函数压入栈中,函数返回前再将函数从栈中取出来执行,最早被压入栈中的函数最晚执行那个。

注意,不仅函数正常金额数会执行 defer 延迟函数,函数中任意一个 return 语句、panic 语句都会触发延迟函数。

defer 语句如何使用

defer 补给你可以用于资源释放,也可以用于流程控制和异常处理,但 defer 关键字只能作用于函数或者函数调用。

defer 关键字后接一个匿名函数:

1
2
3
defer func() {
fmt.Println("Hello World!")
}()

defer 关键字后接一个函数调用

1
2
3
4
5
6
file , err := os.Open(name)
if err != nil {
return nil, err
}

defer file.Close()

defer 的行为规则

在 Go 官方博客中总结了 defer 的行为准则,规则只有短短的 3 条。

1)规则一:延迟函数的参数在 defer 语句出现时就已经确定了

1
2
3
4
5
6
7
func rule1() {
i := 0
defer fmt.Println(i)
i++
return
// print 0
}

defer 语句中的 fmt.Println(i)参数 i 值在 defer 出现时就已经雀帝那个了,实际上是复制了一份。后面对变量 i 的修改不会影响 fmt.Println(i) 函数的执行,所以会打印 0。

2)规则二:延迟函数按后进先出(LIFO)的顺序执行,即先出现的 defer 最后执行

这个规则很好理解,定义 defer 类似于入栈操作,执行 defer 类似于出栈操作。

设计 defer 的初衷是简化函数返回时资源清理的动作,而资源往往有依赖顺序,比如先申请的 A 资源,再根据 A 资源申请 B 资源,根据 B 资源申请 C 资源,即申请顺序是 A-B-C,释放时往往要相反顺序。

3)规则三:延迟函数可能操作主函数的具名返回值

定义 defer 的函数(下面称主函数)可能有返回值,返回值可能有名子(具名返回值),也有可能没有返回值(匿名返回值),延迟函数可能会影响返回值。

若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。

函数中的return 关键字不是一个原子操作,比如 return i 在执行时,它会被为两步执行:

  • 先将 i 值存入栈中作为返回值
  • 然后执行 return

而 defer 语句的执行时机就是在这两步的中间,所以说 defer 语句也是有可能修改返回值的值的。

举个例子:

1
2
3
4
5
6
7
8
9
10
func deferFuncReturn() (return int){
i := 1

defer func () {
result++
}()

return i

}

以上函数的 return 语句可以拆分为下面两行:

1
2
result = i
return

而延迟函数的执行正在 return 之前,所以加入 defer 之后的执行过程如下:

1
2
3
result = i
result++
return

所以上面的函数实际返回 i++。

4)根据返回方式的不同,defer 函数的所造成对返回值的修改也会不同

主函数有不同的返回方式,包括匿名返回值和具名返回值,先看看下面的举例:

(1)主函数拥有匿名返回值,返回字面值

一个主函数拥有匿名返回值,返回时使用字面值,这种情况下 defer 是无法操作返回值的。

1
2
3
4
5
6
7
8
9
10
// 匿名返回值,只有返回类型,没有返回变量名
func foo1() int {
var i int
defer func() {
i++
}

// 直接返回字面值
return 1
}

上面的 return 语句直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以也就无法影响返回值了。

(2)主函数拥有匿名返回值,返回变量

一个函数拥有一个匿名返回值,返回本地或全局变量,这种情况下 defer 语句可以引用返回值,但不会改变返回值。

1
2
3
4
5
6
7
8
func foo2() int {
var i int
defer func() {
i++
}()

return i
}

上面的函数返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假设仍然有一个变量存储返回值,假设返回变量名为 anony,上面的返回语句可以拆分为以下过程:

1
2
3
anony = i
i++
return

由于 i 是整型属于基本类型,所以会将值复制个 anony,所以在 defer 语句中修改 i 值,不会对函数返回值造成影响。

(3)主函数拥有具名返回值

主函数声明语句中大有名字的返回值会被初始化称一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,则可能修改返回值。

1
2
3
4
5
6
7
func foo3() (result int) {
defer func() {
result++
}

return 0
}

上面的函数拆解出来如下所示:

1
2
3
result = 0
result++
return

函数在真正返回请安,在 defer 中对返回值做了+1 操作,所以函数最终返回 1。

总结

什么是 defer 语句

defer 语句用于延迟函数的迪奥用,常用于关闭文件描述符、释放锁等资源释放场景。

defer 如何使用

defer 关键字后接一个匿名函数:

1
2
3
defer func() {
fmt.Println("Hello World!")
}()

defer 关键字后接一个函数调用

1
2
3
4
5
6
file , err := os.Open(name)
if err != nil {
return nil, err
}

defer file.Close()

defer 的行为规则

  1. 规则一:延迟函数的参数在 defer 语句出现时就已经确定了
  2. 规则二:延迟函数按后进先出(LIFO)的顺序执行,即先出现的 defer 最后执行
  3. 规则三:延迟函数可能操作主函数的具名返回值

defer 可能会改变函数的返回值

  1. 主函数拥有匿名返回值,返回字面值

一个主函数拥有匿名返回值,返回时使用字面值,这种情况下 defer 是无法操作返回值的。

  1. 主函数拥有匿名返回值,返回变量

一个函数拥有一个匿名返回值,返回本地或全局变量,这种情况下 defer 语句可以引用返回值,但不会改变返回值。

  1. 主函数拥有具名返回值

主函数声明语句中大有名字的返回值会被初始化称一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,则可能修改返回值。


Go的defer语句
https://sunshinechen.cn/2023/04/01/Go的defer语句/
作者
sunshine
发布于
2023年4月1日
许可协议