什么是defer
在<font style="color:#2D3037;">golang</font>
中,我们使用<font style="color:#2D3037;">defer</font>
语句来进行一些错误处理和收尾工作,它的作用类似<font style="color:#2D3037;">java</font>
里面<font style="color:#2D3037;">finally</font>
关键字的作用
defer
,即延迟调用,是Go
语言的一大特色。defer
代码块会在函数调用链表中增加一个函数调用,在函数正常返回,即return
返回之后,增加一个函数调用。因此,defer常用来回收资源,哪怕程序执行有错误,依然能够保证回收资源等操作能够执行。
为什么需要defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完 毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
defer快速入门
<font style="color:#333333;">defer</font>
后面必须是函数调用语句,不能是其他语句,否则编译器会出错
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
输出结果
hello
world
defer
规则
我们通过以下代码来解释这条规则:
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
上面我们说过,defer 函数会在 return 之后被调用。那么这段函数执行完之后,是不用应该输出 1 呢?
读者自行编译看一下,结果输出的是 0. why?
:::info 这是因为虽然我们在 defer 后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量 (i) 在 defer 被声明的时候,就已经确定其确定的值了。
:::
换言之,上面的代码等同于下面的代码:
func a() {
i := 0
defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
i++
return
}
为了更为明确的说明这个问题,我们继续定义一个 defer:
func a() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0
i++
defer fmt.Println(i) //输出1,因为i此时就是1
return
}
通过运行结果,可以看到 defer 输出的值,就是定义时的值。而不是 defer 真正执行时的变量值 (很重要,搞不清楚的话就会产生于预期不一致的结果)
但为什么是先输出 1,在输出 0 呢? 看下面的规则二。
规则二 defer 执行顺序为先进后出
当同时定义了多个 defer 代码块时,golang 安装先定义后执行的顺序依次调用 defer。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
在循环中,依次定义了四个 defer 代码块。结合规则一,我们可以明确得知每个 defer 代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了 3210.
设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,跟据 B 资源申请 C 资源,即申请顺序是: A—>B—>C,释放时往往又要反向进行。这就是把 deffer 设计成 FIFO 的原因。
每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。
规则三 defer 可以读取有名返回值
先看下面的代码:
func c() (i int) {
defer func() { i++ }()
return 1
}
输出结果是 2. 在开头的时候,我们说过 defer 是在 return 调用之后才执行的。 这里需要明确的是 defer 代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer 的作用域仍然在 c 函数之内。因此 defer 仍然可以读取 c 函数内的变量 (如果无法读取函数内变量,那又如何进行变量清除呢….)。
当执行 return 1 之后,i 的值就是 1. 此时此刻,defer 代码块开始执行,对 i 进行自增操作。 因此输出 2.
掌握了 defer 以上三条使用规则,那么当我们遇到 defer 代码块时,就可以明确得知 defer 的预期结果
函数返回过程
有一个事实必须要了解,关键字_return_
不是一个原子操作,实际上_return_
只代理汇编指令_ret_
,即将跳转程序执行。比如语句return i
,实际上分两步进行,即将<font style="color:#333333;">i</font>
值存入栈中作为返回值,然后执行跳转,而<font style="color:#333333;">defer</font>
的执行时机正是跳转前,所以说<font style="color:#333333;">defer</font>
执行时还是有机会操作返回值的。
举个实际的例子进行说明这个过程:
func deferFuncReturn() (result int) { i := 1
defer func() {
result++
}() return i}
该函数的 return 语句可以拆分成下面两行:
result = i
return
而延迟函数的执行正是在 return 之前,即加入 defer 后的执行过程如下:
result = i //将return 后的变量赋给result
result++ //执行defer逻辑
return //真正的return
所以上面函数实际返回 i++ 值。
关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把 return 语句拆开都可以很好的理解,
3.3.1 主函数拥有匿名返回值,返回字面值
一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 “1”、”2”、”Hello” 这样的值,这种情况下 defer 语句是无法操作返回值的。
一个返回字面值的函数,如下所示:
func foo() int { var i int
defer func() {
i++
}() return 1}
上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。
3.3.2 主函数拥有匿名返回值,返回变量
一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下 defer 语句可以引用到返回值,但不会改变返回值。
一个返回本地变量的函数,如下所示:
func foo() int { var i int
defer func() {
i++
}() return i
}
上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为 “anony”,上面的返回语句可以拆分成以下过程:
anony = i
i++
return
由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响。
3.3.3 主函数拥有具名返回值
主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。
一个影响函返回值的例子:
func foo() (ret int) { defer func() {
ret++
}() return 0}
上面的函数拆解出来,如下所示:
ret = 0
ret++
return
函数真正返回前,在 defer 中对返回值做了 + 1 操作,所以函数最终返回 1。
defer 内部实现原理
本节我们尝试了解一些 defer 的实现机制。
4.1 defer 数据结构
源码包src/src/runtime/runtime2.go:_defer
定义了 defer 的数据结构:
type _defer struct {
sp uintptr //函数栈指针
pc uintptr //程序计数器
fn *funcval //函数地址
link *_defer //指向自身结构的指针,用于链接多个defer}
我们知道 defer 后面一定要接一个函数的,所以 defer 的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。
与函数不同的一点是它含有一个指针,可用于指向另一个 defer,每个 goroutine 数据结构中实际上也有一个 defer 指针,该指针指向一个 defer 的单链表,每次声明一个 defer 时就将 defer 插入到单链表表头,每次执行 defer 时就从单链表表头取出一个 defer 执行。
下图展示一个 goroutine 定义多个 defer 时的场景:
从上图可以看到,新声明的 defer 总是添加到链表头部。
函数返回前执行 defer 则是从链表首部依次取出执行,不再赘述。
一个 goroutine 可能连续调用多个函数,defer 添加过程跟上述流程一致,进入函数时添加 defer,离开函数时取出 defer,所以即便调用多个函数,也总是能保证 defer 是按 FIFO 方式执行的。
4.2 defer 的创建和执行
源码包src/runtime/panic.go
定义了两个方法分别用于创建 defer 和执行 defer。
- deferproc(): 在声明 defer 处调用,其将 defer 函数存入 goroutine 的链表中;
- deferreturn():在 return 指令,准确的讲是在 ret 指令前调用,其将 defer 从 goroutine 链表中取出并执行。
可以简单这么理解,在编译在阶段,声明 defer 处插入了函数 deferproc(),在函数 return 前插入了函数 deferreturn()。
defer 最佳实践
在 golang 当中,defer 代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是 return 之后添加一个函数调用。因此,defer 通常用来释放函数内部变量。
defer 类似于java里面的finally
为了更好的学习 defer 的行为,我们首先来看下面一段代码:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
这段代码可以运行,但存在’安全隐患’。如果调用 dst, err := os.Create(dstName)
失败,则函数会执行 return
退出运行。但之前创建的 src
(文件句柄) 没有被释放。 上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。 如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。 而使用 defer
则可以避免这种情况的发生,下面是使用 defer
的代码:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
通过 defer,我们可以在代码中优雅的关闭 / 清理代码中所使用的变量。
总结
- defer定义的延迟函数参数在defer语句出时就已经确定下来了
- defer定义顺序与实际执行顺序相反
- return不是原子操作,执行过程是: 保存返回值(若有)—>执行defer(若有)—>执行ret跳转
- 申请资源后立即使用defer关闭资源是好习惯