1. defer 定义
Go
函数的关键字 defer
可以提供注册多个延迟调用,只能出现在函数内部,在 defer
归属的函数即将返回时,将延迟处理的语句按 defer
的逆序进行执行,这些调用遵循先进后出的顺序在函数返回前被执行。也就是说,先被 defer
的语句最后被执行,最后被 defer
的语句,最先被执行。
defer
常用于保证一些资源最终一定能够得到释放或者回收。
2. defer 使用
代码示例:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("First")
}()
defer func() {
fmt.Println("Second")
}()
// defer 后面必须是函数或者方法的调用,否则报错:
// expression in defer must be function call
fmt.Println("This is main func body")
}
输出:
This is main func body
Second
First
3. defer 特点
3.1 defer 实参使用值拷贝传递
defer
函数的实参在注册时使用值拷贝传递进去,即 defer
后面的函数参数会被实时解析;
package main
import "fmt"
func main() {
a := 10
defer func(i int) {
fmt.Println("defer func i is ", i) // defer func i is 10
}(a)
a += 10
fmt.Println("after defer a is ", a) // after defer a is 20
}
可以看到后面的 a += 10
并不影响 defer
函数的结果。
3.2 defer 必须先注册
defer
函数必须先注册才能执行,如果 defer
位于 return
语句之后,因为 defer
没有注册,不会被执行;
package main
import "fmt"
func main() {
defer func() {
fmt.Println("First")
}()
return
// 后面的均不会执行
defer func() {
fmt.Println("Second")
}()
fmt.Println("This is main func body")
}
输出:
First
3.3 defer 遇到 os.Exit
当主动调用 os.Exit(int)
退出进程时, defer
即使已经注册,那么也不再被执行。
package main
import (
"fmt"
"os"
)
func main() {
defer func() {
fmt.Println("First")
}()
fmt.Println("This is main func body")
os.Exit(1)
}
输出:
This is main func body
exit status 1
3.4 defer 语句放到错误检查语句之后
一般 defer
语句放到错误检查语句之后;
3.5 defer 尽量不要放到循环语句内部
defer
尽量不要放到循环语句内部;
3.6 defer 性能损耗
defer
相对普通函数调用有一定的性能损耗,具体参考 Go defer 会有性能损耗,尽量不能用?;
3.7 defer 用于资源释放和错误处理
defer
通常用于释放资源或错误处理。
package main
import "os"
func test() error {
f, err := os.Create("test.txt")
if err != nil {
return err
}
defer f.Close() // 注册调用,而不是注册函数。必须提供参数,哪怕为空。
f.WriteString("Hello, World!")
return nil
}
func main() {
test()
}
3.8 多个 defer 注册,按 FILO 次序执行
多个 defer
注册,按 FILO
次序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
输出:
c
b
a
panic: runtime error: integer divide by zero
3.9 延迟调用参数在注册时求值或复制
延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。
package main
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
}
func main() {
test()
}
输出:
x = 20 y = 120
defer: 10 120
defer
关键字后面的表达式,是在将 deferred
函数注册到 deferred
函数栈的时候进行求值的。
我们同样用一个典型的例子来说明一下 defer
后表达式的求值时机:
func foo1() {
for i := 0; i <= 3; i++ {
defer fmt.Println(i)
}
}
func foo2() {
for i := 0; i <= 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i)
}
}
func foo3() {
for i := 0; i <= 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
func main() {
fmt.Println("foo1 result:")
foo1()
fmt.Println("\nfoo2 result:")
foo2()
fmt.Println("\nfoo3 result:")
foo3()
}
这里,我们一个个分析 foo1、foo2 和 foo3 中 defer
后的表达式的求值时机。
首先是 foo1。foo1 中 defer
后面直接用的是 fmt.Println
函数,每当 defer
将 fmt.Println
注册到 deferred
函数栈的时候,都会对 Println
后面的参数进行求值。根据上述代码逻辑,依次压入 deferred
函数栈的函数是:
fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
因此,当 foo1 返回后,deferred
函数被调度执行时,上述压入栈的 deferred
函数将以 LIFO
次序出栈执行,这时的输出的结果为:
3
2
1
0
然后我们再看 foo2。foo2 中 defer
后面接的是一个带有一个参数的匿名函数。每当 defer
将匿名函数注册到 deferred
函数栈的时候,都会对该匿名函数的参数进行求值。根据上述代码逻辑,依次压入 deferred
函数栈的函数是:
func(0)
func(1)
func(2)
func(3)
因此,当 foo2 返回后,deferred
函数被调度执行时,上述压入栈的 deferred
函数将以 LIFO
次序出栈执行,因此输出的结果为:
3
2
1
0
最后我们来看 foo3。foo3 中 defer
后面接的是一个不带参数的匿名函数。根据上述代码逻辑,依次压入 deferred
函数栈的函数是:
func()
func()
func()
func()
所以,当 foo3 返回后,deferred
函数被调度执行时,上述压入栈的 deferred
函数将以 LIFO
次序出栈执行。匿名函数会以闭包的方式访问外围函数的变量 i
,并通过 Println
输出 i
的值,此时 i
的值为 4,因此 foo3 的输出结果为:
4
4
4
4
通过这些例子,我们可以看到,无论以何种形式将函数注册到 defer
中,deferred
函数的参数值都是在注册的时候进行求值的。
3.10 滥用 defer 可能会导致性能问题
滥用 defer
可能会导致性能问题,尤其是在一个 “大循环” 里。
defer
是在函数退出时调用的。如果在 for
语句的每个迭代都使用 defer
设置 deferred
函数,这些deferred
函数会压入 runtime
实现的 defer
列表中。会占用内存资源,并且如果 for
的 loop
次数很多,这个消耗将很可观。
3.11 defer 返回值被丢弃
defer
后边调用的函数如果有返回值,则这个返回值将会被丢弃。
package main
import "fmt"
func demo() int {
defer func() int {
return 100
}()
return 8
}
func main() {
fmt.Println(demo()) // 8
}
上边的示例代码中,demo 函数的返回值是 8。defer
后边调用的函数的返回值并不能作为 demo 函数的返回值。
3.12 defer 改变有名返回参数的值
defer
可以改变有名返回参数的值
这是由于在 Go
语言中,return
是函数的返回标志,并不代表执行结束。return
语句并不是原子操作,最先为返回值赋值,然后执行 defer
命令,最后才是真正意义上的 return
操作。
如果是有名返回值,返回值变量其实可视为是引用赋值,可以能被 defer
修改。而在匿名返回值时,给 ret
的值相当于拷贝赋值,defer
命令时不能直接修改。
有名返回值:
func demo() (i int)
上面函数签名中的 i
就是有名返回值,如果 demo()
中定义了 defer
代码块,是可以改变返回值 i
的,函数返回语句 return i
可以简写为 return
。
这里综合了以上几种情况,在下面这个例子里列举了几种情况,
package main
import (
"fmt"
)
func main() {
fmt.Println("=========================")
fmt.Println("return:", fun1())
fmt.Println("=========================")
fmt.Println("return:", fun2())
fmt.Println("=========================")
fmt.Println("return:", fun3())
fmt.Println("=========================")
fmt.Println("return:", fun4())
}
func fun1() (i int) {
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer2: 2
}()
// 规则二 defer执行顺序为先进后出
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer1: 1
}()
// 规则三 defer可以改变有名返回参数的值
return 0 //这里实际结果为2。如果是return 100呢
}
func fun2() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer2: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer1: 1
}()
return i
}
func fun3() (r int) {
t := 5
defer func() {
t = t + 5
fmt.Println(t)
}()
return t
}
func fun4() int {
i := 8
// 规则一 defer后面的函数参数会被实时解析
defer func(i int) {
i = 99
fmt.Println(i)
}(i)
i = 19
return i
}
在上面 fun1() (i int)
有名返回值情况下,return
最终返回的实际值和期望的 return 0
有较大出入。
因为在上面 fun1() (i int)
中,如果 return 100
或 return 0
,这样的区别在于 i
的值实际上分别是 100 或 0。而在上面中,如果 return 100
,则因为改变了有名返回值 i,而 defer
可以读取有名返回值,所以返回值最终为 102,而 defer1
打印 101,defer
打印 102。因此一般直接写为 return
。
这点要注意,有时函数可能返回非我们希望的值,所以改为匿名返回也是一种办法。具体请看下面输出。
=========================
defer1: 1
defer2: 2
return: 2
=========================
defer1: 1
defer2: 2
return: 0
=========================
10
return: 5
=========================
99
return: 19
4. defer 释放资源
处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中,最容易忽略的就是在每个函数退出处正确地释放和关闭资源。defer
语句正好是在函数退出时执行的语句,所以使用 defer
能非常方便地处理资源释放问题。
4.1 使用延迟并发解锁
在下面的例子中会在函数中并发使用 map
,为防止竞态问题,使用 sync.Mutex
进行加锁,参见下面代码:
var (
// 一个演示用的映射
valueByKey = make(map[string]int)
// map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。
// 保证使用映射时的并发安全的互斥锁
valueByKeyGuard sync.Mutex
)
// 根据键读取值
func readValue(key string) int {
// 对共享资源加锁
valueByKeyGuard.Lock()
// 取值
v := valueByKey[key]
// 对共享资源解锁
valueByKeyGuard.Unlock()
// 返回值
return v
}
使用 defer 语句对上面的语句进行简化,参考下面的代码。
func readValue(key string) int {
valueByKeyGuard.Lock()
// defer后面的语句不会马上调用, 而是延迟到函数结束时调用
defer valueByKeyGuard.Unlock()
return valueByKey[key]
}
4.2 使用延迟释放文件句柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源。
在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源,参考下面的代码:
// 根据文件名查询其大小
func fileSize(filename string) int64 {
// 根据文件名打开文件, 返回文件句柄和错误
f, err := os.Open(filename)
// 如果打开时发生错误, 返回文件大小为0
if err != nil {
return 0
}
// 取文件状态信息
info, err := f.Stat()
// 如果获取信息时发生错误, 关闭文件并返回文件大小为0
if err != nil {
f.Close()
return 0
}
// 取文件大小
size := info.Size()
// 关闭文件
f.Close()
// 返回文件大小
return size
}
在上面的例子中,第 25 行是对文件的关闭操作,下面使用 defer 对代码进行简化,代码如下:
func fileSize(filename string) int64 {
f, err := os.Open(filename)
if err != nil {
return 0
}
// 延迟调用Close, 此时Close不会被调用
defer f.Close() // defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。
// 不能将这一句代码放在第 4 行空行处,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
info, err := f.Stat()
if err != nil {
// defer机制触发, 调用Close关闭文件
return 0
}
size := info.Size()
// defer机制触发, 调用Close关闭文件
return size
}