Go函数篇--延迟调用defer

延迟调用defer

一、基本概念

1.1.defer的定义和场景

defer的格式:

1
DeferStmt = "defer" Expression .

表达式(Expression) 必须是a function or a method call,不能是parenthesized(括号起来的),也不能是内置函数

defer是用于延迟执行defer后面的表达式,有三个场景会触发

  • 周围函数执行了一个return语句
  • 周围函数执行到了函数体的尾部
  • 相应的goroutine is panicking

什么又是延迟执行呢?
每次执行到defer的时候,不会直接执行实际的函数(Expression),而是保存起来(用栈保存的吗?),然后等到触发条件达成的时候,依次先进后出地执行,执行的时机是

  • 设置任何结果参数之后,就是给返回值赋值
  • 函数返回给其调用者之前
1
2
3
4
5
6
7
8
# 官方文档,不理解的部分

If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the "defer" statement is executed.
如果延迟函数值的计算结果为 nil,则在调用函数时执行恐慌,而不是在执行“延迟”语句时

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned. If the deferred function has any return values, they are discarded when the function completes. (See also the section on handling panics.)

例如,如果延迟函数是函数字面量,并且周围的函数具有在字面量范围内的命名结果参数,则延迟函数可以在返回结果参数之前访问和修改结果参数。如果延迟函数有任何返回值,它们会在函数完成时被丢弃。 (另请参阅处理恐慌的部分。)

举个例子

1
2
3
4
5
6
7
8
9
10
11
lock(l)
unlock(l)

// 返回100
func f()(result int){
defer func(){
result *= 10
}()
return result + 10
}

因此,defer适用于 关闭一些已经打开的资源,例如文件句柄数据库连接

1.2.defer的实现原理

defer调用runtime.deferproc,
在defer执行的地方插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执

二、defer与return

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 返回的是1
func f1()(r int){
defer func (){
r = r+1
}
return 0
}

// 返回的是5
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}

// 返回的是1
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}

这里要把return这个指令变成3步,就很好理解了:

  1. 给返回参数r赋值
  2. 执行defer函数
  3. 执行“裸”return

以第二个为例:

1
2
3
4
5
6
7
8
9
10
func f() (r int) {
t := 5

// 执行return和defer
r = t
func() {
t = t + 5
}()
return r
}

当然,如果没有标识返回值参数变量r,也是一样的效果,猜测内部是使用了一个类似tmp的变量进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func f() int {
t := 5
defer func() {
t = t + 5
}()
return t
}


func f() int {
t := 5

// 执行return和defer
tmp = t
func() {
t = t + 5
}()
return tmp
}

三、defer和闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

func main() {
var w [5]int
for i := range w {
// 一般defer不会放在for里面使用,编译器会有警告信息
// Possible resource leak, 'defer' is called in the 'for' loop
defer func() { fmt.Println(i) }()
}
}

// 输出结果:
4 4 4 4 4
因为闭包用到的变量i,在执行的时候已经变成4了,因此defer调用的时候都是4



TODO:这个例子还得看一下闭包。
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遇到错误

defer会按先进后出的顺序执行,如果遇到了错误,依然继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

五、defer nil函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}

func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}

// 输出结果
runs
runtime error: invalid memory address or nil pointer dereference

说明:defer遇到nil的时候,会发生panic

六、在错误的位置使用defer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "net/http"

func do() error {
res, err := http.Get("http://www.google.com")
defer res.Body.Close()
if err != nil {
return err
}

// ..code...

return nil
}

func main() {
do()
}

// 输出结果

panic: runtime error: invalid memory address or nil pointer dereference

因为Get没有执行成功,然后res是nil,会抛出异常,可以在err判断前加上这个

1
2
3
if res != nil {
defer res.Body.Close()
}

七、defer实现函数入口和出口处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
)

func doSomething() {
defer printTimeUsage("doSomething")()
time.Sleep(5 * time.Second)
}

func printTimeUsage(funcName string) func() {
start := time.Now()
fmt.Printf("Func %s start.\n", funcName)
return func() {
fmt.Printf("Func %s end, time usage: %s.\n", funcName, time.Since(start))
}
}

func main() {
doSomething()
}

输出结果:

1
2
Func doSomething start.
Func doSomething end, time usage: 5.00141s.

八、在循环中使用defer

要注意,defer只有在函数执行完成或者错误的时候才会执行,以下代码可能会导致 文件描述符 被耗尽

1
2
3
4
5
6
7
8
9

for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil{
return err
}
defer f.Close()
}

可以把打开和关闭文件,放到一个函数里面去执行,这样在每次循环中打开文件之后就能及时关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for _, filename := range filenames {
if f, err := doOpenFile(filename); err != nil{
return err
}
// ...
}

func doOpenFile(filename) err{
f, err := os.Open(filename)
if err != nil {
return nil, err
}

defer f.Close()
return f, nil
}

Refs