Random walk to my blog

my blog for sharing my knowledge,experience and viewpoint

0%

Goalng defer解析:用法,源码分析

defer语句是在函数返回之前,执行一段代码,正如在Golang 规范描述的:

Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred.(defer函数在它被返回之前进行调用,以defer声明的逆序调用)

看下面的一个例子:

1
2
3
4
5
6
7
8
func main() {
defer func() {
println("World")
}()
defer func() {
println("Hello")
}()
}

运行结果是

1
2
Hello
World

defer的参数传递

传递给defer的参数有2中方式

  1. 函数参数。当defer声明的时候,将值传给defer。这个值会马上计算,后续defer执行,也会使用这个值。
  2. 闭包引用。defer执行使用的是变量实际的值。
    看下面例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func TestClosure(t *testing.T) {
    // 值传递,方式1
    for i := 0; i < 3; i++ {
    defer fmt.Println("defer param: ", i)
    }
    // 函数参数,方式1
    for i := 0; i < 3; i++ {
    defer func(data int) {
    fmt.Println("defer func param: ", data)
    }(i)
    }
    // 闭包引用,方式2
    for i := 0; i < 3; i++ {
    defer func() {
    fmt.Println("closure param: ", i)
    }()
    }
    }
    运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    closure param:  3
    closure param: 3
    closure param: 3
    defer func param: 2
    defer func param: 1
    defer func param: 0
    defer param: 2
    defer param: 1
    defer param: 0

defer与函数返回值

defer的函数能够修改返回值的唯一方式是使用命名的返回参数
正如Golang规范里面说的

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

看下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestDeferParam(t *testing.T) {
fmt.Printf("with named param, x: %d\n", withNamedParam())
fmt.Printf("without named param, x: %d\n", withoutNamedParam())
}
func withNamedParam() (x int) {
x = 99
defer func() { x = 100 }()
return x
}

func withoutNamedParam() int {
x := 99
defer func() { x = 100 }()
return x
}

运行结果

1
2
with named param, x: 100
without named param, x: 99

下面是defer,recover,panic结合使用的例子,来源于Go blog.

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"

func main() {
f()
fmt.Println("Returned normally from f.")
}

func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}

func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}

panic是一个内置函数,它结束当前的流程控制。当一个函数调用panic时,函数停止执行,函数里面的defer函数正常执行,然后返回到调用者。这个过程会沿着调用栈往上传递,直到goroutine的所有函数都已经返回。
recover是一个内置函数,将控制权转回给panic的goroutine。Recover只有在defer的函数里面才用作用。
执行结果

1
2
3
4
5
6
7
8
9
10
11
12
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

defer源码

在Go里面,defer是一个类似链表的数据结构, 以LIFO(先入后出)的方式来实现的。

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 // next deferred function to be executed
}

当新的defer函数创建时,它被绑定在当前的Goroutine。上一个defer函数的link指针指向下一个defer函数。

1
2
3
4
5
6
7
8
9
func newdefer(siz int32) *_defer {
var d *_defer
gp := getg() // get the current goroutine
[...]
// deferred list is now attached to the new _defer struct
d.link = gp._defer
gp._defer = d // the new struct is now the first to be called
return d
}

defer函数的调用会以出栈的方式进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
func deferreturn(arg0 uintptr) {
gp := getg() // get the current goroutine
d:= gp._defer // copy the deferred function to a variable
if d == nil { // if there is not deferred func, just return
return
}
[...]
fn := d.fn // get the function to call
d.fn = nil // reset the function
gp._defer = d.link // attach the next one to the goroutine
freedefer(d) // freeing the _defer struc
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // call the func
}

上面的代码显示,Golang没有怼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
42
cat defer.go
package main

func main() {
defer func() {
println("World")
}()
defer func() {
println("Hello")
}()
}
go build -gcflags -S defer.go
go tool objdump -s main.main defer
// first deferred func
0x001d 00029 (main.go:6) MOVL $0, (SP)
0x0024 00036 (main.go:6) PCDATA $2, $1
0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:6) PCDATA $2, $0
0x002b 00043 (main.go:6) MOVQ AX, 8(SP)
0x0030 00048 (main.go:6) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:6) TESTL AX, AX
0x0037 00055 (main.go:6) JNE 117
// second deferred func
0x0039 00057 (main.go:10) MOVL $0, (SP)
0x0040 00064 (main.go:10) PCDATA $2, $1
0x0040 00064 (main.go:10) LEAQ "".main.func2·f(SB), AX
0x0047 00071 (main.go:10) PCDATA $2, $0
0x0047 00071 (main.go:10) MOVQ AX, 8(SP)
0x004c 00076 (main.go:10) CALL runtime.deferproc(SB)
0x0051 00081 (main.go:10) TESTL AX, AX
0x0053 00083 (main.go:10) JNE 101
// end of main func
0x0055 00085 (main.go:18) XCHGL AX, AX
0x0056 00086 (main.go:18) CALL runtime.deferreturn(SB)
0x005b 00091 (main.go:18) MOVQ 16(SP), BP
0x0060 00096 (main.go:18) ADDQ $24, SP
0x0064 00100 (main.go:18) RET
0x0065 00101 (main.go:10) XCHGL AX, AX
0x0066 00102 (main.go:10) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:10) MOVQ 16(SP), BP
0x0070 00112 (main.go:10) ADDQ $24, SP
0x0074 00116 (main.go:10) RET

可以看到deferproc被调用了2次。