Just Do IT !

Golang语言学习从入门到实战----defer

字数统计: 862阅读时长: 3 min
2020/03/07 Share

defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)

简单的demo

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

import "fmt"

func main() {
res := sum(10, 20)
fmt.Println("number4 res=", res)
}

func sum(n1 int, n2 int) int {
// 函数执行完毕后,及时的释放资源
// 先进后出
defer fmt.Println("number1 n1 = ", n1)
defer fmt.Println("number2 n2 = ", n2)

res := n1 + n2
fmt.Println("number3 res = ", res) // 最先执行
return res
}

输出:

number3 res =  30
number2 n2 =  20
number1 n1 =  10
number4 res= 30
  • go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个”栈”(比喻)中, 然后继续执行函数下一个语句。
  • 当函数执行完毕后,在从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
    25
    26
    27
    28
    29
    30
    package main

    import "fmt"

    /*
    输出:
    number3 res = 30
    number2 n2 = 20
    number1 n1 = 10
    number4 res= 30
    */
    func main() {
    res := sum(10, 20)
    fmt.Println("number4 res=", res)
    }

    func sum(n1 int, n2 int) int {
    // 函数执行完毕后,及时的释放资源
    // 先进后出
    defer fmt.Println("number1 n1 = ", n1)
    defer fmt.Println("number2 n2 = ", n2)

    // 增加一段
    n1++
    n2++

    res := n1 + n2
    fmt.Println("number3 res = ", res) // 最先执行
    return res
    }

输出:

number3 res =  32 // 输出结果为32
number2 n2 =  20 // 栈中的数值仍是存入前的数值
number1 n1 =  10
number4 res= 32

使用defer+recover来处理错误

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
package main

import (
"fmt"
"time"
)

func main() {

test()
for {
fmt.Println("main()下面的代码...")
time.Sleep(time.Second)
}
}

func test() {
defer func() {
err := recover() // recover()内置函数,可以捕获到异常
if err != nil { // 捕获到异常
fmt.Println("err=", err)
}
}()

num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}

输出:

err= runtime error: integer divide by zero
main()下面的代码...

defer在Go中的数据结构

1
2
3
4
5
6
7
8
9
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}

runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link字段串联成链表。

在这里插入图片描述

  • siz 是参数和结果的内存大小;
  • sp 和 pc 分别代表栈指针和调用方的程序计数器;
  • fn 是 defer 关键字中传入的函数;
  • _panic 是触发延迟调用的结构体,可能为空;

除了上述的这些字段之外,runtime._defer 中还包含一些垃圾回收机制使用的字段,这里为了减少理解的成本就都省去了

小结

defer关键字的实现主要依靠编译器和运行时的协作

编译期;

defer 关键字被转换runtime.deferproc
在调用defer 关键字的函数返回之前插入runtime.deferreturn;

运行时:

runtime.deferproc会将一个新的 runtime._defer结构体追加到当前Goroutine的链表头;
runtime.deferreturn会从 Goroutine的链表中取出runtime._defer 结构并依次执行;

  • 后调用的defer函数会先执行:
    • 后调用的defer函数会被追加到Goroutine _defer链表的最前面;
    • 运行runtime._defer时是从前到后依次执行;
  • 函数的参数会被预先计算;
    • 调用runtime.deferproc函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;

更多关于Golang defer的编译过程与运行过程可以参考:

参考链接:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/#533-

CATALOG
  1. 1. defer
    1. 1.1. 简单的demo
    2. 1.2. 使用defer+recover来处理错误
    3. 1.3. defer在Go中的数据结构
    4. 1.4. 小结
      1. 1.4.1. 编译期;
      2. 1.4.2. 运行时: