1. defer的使用
defer 延遲調用。我們先來看一下,有defer關鍵字的代碼執行順序:
1 func main() {
2 defer func() {
3 fmt.Println("1號輸出")
4 }()
5 defer func() {
6 fmt.Println("2號輸出")
7 }()
8 }
輸出結果:
1 2號出來
2 1號出來
結論:多個defer的執行順序是倒序執行(同入棧先進后出)。
由例子可以看出來,defer有延遲生效的作用,先使用defer的語句延遲到最後執行。
1.1 defer與返回值之間的順序
1 func defertest() int
2
3 func main() {
4 fmt.Println("main:", defertest())
5 }
6
7 func defertest() int {
8 var i int
9 defer func() {
10 i++
11 fmt.Println("defer2的值:", i)
12 }()
13 defer func() {
14 i++
15 fmt.Println("defer1的值:", i)
16 }()
17 return i
18 }
輸出結果:
1 defer1的值: 1
2 defer2的值: 2
3 main: 0
結論:return最先執行->return負責將結果寫入返回值中->接着defer開始執行一些收尾工作->最後函數攜帶當前返回值退出
return的時候已經先將返回值給定義下來了,就是0,由於i是在函數內部聲明所以即使在defer中進行了++操作,也不會影響return的時候做的決定。
1 func test() (i int)
2
3 func main() {
4 fmt.Println("main:", test())
5 }
6
7 func test() (i int) {
8 defer func() {
9 i++
10 fmt.Println("defer2的值:", i)
11 }()
12 defer func() {
13 i++
14 fmt.Println("defer1的值:", i)
15 }()
16 return i
17 }
詳解:由於返回值提前聲明了,所以在return的時候決定的返回值還是0,但是後面兩個defer執行後進行了兩次++,將i的值變為2,待defer執行完后,函數將i值進行了返回。
2. defer定義和執行
1 func test(i *int) int {
2 return *i
3 }
4
5 func main(){
6 var i = 1
7
8 // defer定義的時候test(&i)的值就已經定了,是1,後面就不會變了
9 defer fmt.Println("i1 =" , test(&i))
10 i++
11
12 // defer定義的時候test(&i)的值就已經定了,是2,後面就不會變了
13 defer fmt.Println("i2 =" , test(&i))
14
15 // defer定義的時候,i就已經確定了是一個指針類型,地址上的值變了,這裏跟着變
16 defer func(i *int) {
17 fmt.Println("i3 =" , *i)
18 }(&i)
19
20 // defer定義的時候i的值就已經定了,是2,後面就不會變了
21 defer func(i int) {
22 //defer 在定義的時候就定了
23 fmt.Println("i4 =" , i)
24 }(i)
25
26 defer func() {
27 // 地址,所以後續跟着變
28 var c = &i
29 fmt.Println("i5 =" , *c)
30 }()
31
32 // 執行了 i=11 后才調用,此時i值已是11
33 defer func() {
34 fmt.Println("i6 =" , i)
35 }()
36
37 i = 11
38 }
結論:會先將defer后函數的參數部分的值(或者地址)給先下來【你可以理解為()裡頭的會先確定】,後面函數執行完,才會執行defer后函數的{}中的邏輯。
例題分析
1 //例子1
2 func f() (result int) {
3 defer func() {
4 result++
5 }()
6 return 0
7 }
8 //例子2
9 func f() (r int) {
10 t := 5
11 defer func() {
12 t = t + 5
13 }()
14 return t
15 }
16 //例子3
17 func f() (r int) {
18 defer func(r int) {
19 r = r + 5
20 }(r)
21 return 1
22 }
例1的正確答案不是0,例2的正確答案不是10,例3的正確答案不是6……
這裏先說一下返回值。defer是在return之前執行的。這條規則毋庸置疑,但最重要的一點是要明白,return xxx這一條語句並不是一條原子指令!
函數返回的過程:先給返回值賦值,然後調用defer表達式,最後才是返回到調用函數中。defer表達式可能會在設置函數返回值之後,且在返回到調用函數之前去修改返回值,使最終的函數返回值與你想象的不一致。
return xxx 可被改寫成:
1 返回值 = xxx
2 調用defer函數
3 空的return
所以例子也可以改寫成:
1 //例1
2 func f() (result int) {
3 result = 0 //return語句不是一條原子調用,return xxx其實是賦值+ret指令
4 func() { //defer被插入到return之前執行,也就是賦返回值和ret指令之間
5 result++
6 }()
7 return
8 }
9 //例2
10 func f() (r int) {
11 t := 5
12 r = t //賦值指令
13 func() { //defer被插入到賦值與返回之間執行,這個例子中返回值r沒被修改過
14 t = t + 5
15 }
16 return //空的return指令
17 }
18 例3
19 func f() (r int) {
20 r = 1 //給返回值賦值
21 func(r int) { //這裏改的r是傳值傳進去的r,不會改變要返回的那個r值
22 r = r + 5
23 }(r)
24 return //空的return
25 }
所以例1的結果是1,例2的結果是5,例3的結果是1.
3. defer內部原理
從例子開始看:
1 packmage main
2
3 import()
4
5 func main() {
6 defer println("這是一個測試")
7 }
反編譯一下看看:
1 src $ go build -o test test.go
2 src $ go tool objdump -s "main\.main" test
1 TEXT main.main(SB) /Users/tushanshan/go/src/test3.go
2 test3.go:5 0x104ea70 65488b0c2530000000 MOVQ GS:0x30, CX
3 test3.go:5 0x104ea79 483b6110 CMPQ 0x10(CX), SP
4 test3.go:5 0x104ea7d 765f JBE 0x104eade
5 test3.go:5 0x104ea7f 4883ec28 SUBQ $0x28, SP
6 test3.go:5 0x104ea83 48896c2420 MOVQ BP, 0x20(SP)
7 test3.go:5 0x104ea88 488d6c2420 LEAQ 0x20(SP), BP
8 test3.go:6 0x104ea8d c7042410000000 MOVL $0x10, 0(SP)
9 test3.go:6 0x104ea94 488d05e5290200 LEAQ go.func.*+57(SB), AX
10 test3.go:6 0x104ea9b 4889442408 MOVQ AX, 0x8(SP)
11 test3.go:6 0x104eaa0 488d05e6e50100 LEAQ go.string.*+173(SB), AX
12 test3.go:6 0x104eaa7 4889442410 MOVQ AX, 0x10(SP)
13 test3.go:6 0x104eaac 48c744241804000000 MOVQ $0x4, 0x18(SP)
14 test3.go:6 0x104eab5 e8b631fdff CALL runtime.deferproc(SB)
15 test3.go:6 0x104eaba 85c0 TESTL AX, AX
16 test3.go:6 0x104eabc 7510 JNE 0x104eace
17 test3.go:7 0x104eabe 90 NOPL
18 test3.go:7 0x104eabf e83c3afdff CALL runtime.deferreturn(SB)
19 test3.go:7 0x104eac4 488b6c2420 MOVQ 0x20(SP), BP
20 test3.go:7 0x104eac9 4883c428 ADDQ $0x28, SP
21 test3.go:7 0x104eacd c3 RET
22 test3.go:6 0x104eace 90 NOPL
23 test3.go:6 0x104eacf e82c3afdff CALL runtime.deferreturn(SB)
24 test3.go:6 0x104ead4 488b6c2420 MOVQ 0x20(SP), BP
25 test3.go:6 0x104ead9 4883c428 ADDQ $0x28, SP
26 test3.go:6 0x104eadd c3 RET
27 test3.go:5 0x104eade e8cd84ffff CALL runtime.morestack_noctxt(SB)
28 test3.go:5 0x104eae3 eb8b JMP main.main(SB)
29 :-1 0x104eae5 cc INT $0x3
30 :-1 0x104eae6 cc INT $0x3
31 :-1 0x104eae7 cc INT $0x3
編譯器將defer處理成兩個函數調用 deferproc 定義一個延遲調用對象,然後在函數結束前通過 deferreturn 完成最終調用。在defer出現的地方,插入了指令call runtime.deferproc,然後在函數返回之前的地方,插入指令call runtime.deferreturn。
內部結構
1 //defer
2 type _defer struct {
3 siz int32 // 參數的大小
4 started bool // 是否執行過了
5 sp uintptr // sp at time of defer
6 pc uintptr
7 fn *funcval
8 _panic *_panic // defer中的panic
9 link *_defer // defer鏈表,函數執行流程中的defer,會通過 link這個 屬性進行串聯
10 }
11 //panic
12 type _panic struct {
13 argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
14 arg interface{} // argument to panic
15 link *_panic // link to earlier panic
16 recovered bool // whether this panic is over
17 aborted bool // the panic was aborted
18 }
19 //g
20 type g struct {
21 _panic *_panic // panic組成的鏈表
22 _defer *_defer // defer組成的先進后出的鏈表,同棧
23 }
因為 defer panic 都是綁定在運行的g上的,這裏也說一下g中與 defer panic相關的屬性
再把defer, panic, recover放一起看一下:
1 func main() {
2 defer func() {
3 recover()
4 }()
5 panic("error")
6 }
反編譯結果:
1 go build -gcflags=all="-N -l" main.go
2 go tool objdump -s "main.main" main
1 go tool objdump -s "main\.main" main | grep CALL
2 main.go:4 0x4548d0 e81b00fdff CALL runtime.deferproc(SB)
3 main.go:7 0x4548f2 e8b90cfdff CALL runtime.gopanic(SB)
4 main.go:4 0x4548fa e88108fdff CALL runtime.deferreturn(SB)
5 main.go:3 0x454909 e85282ffff CALL runtime.morestack_noctxt(SB)
6 main.go:5 0x4549a6 e8d511fdff CALL runtime.gorecover(SB)
7 main.go:4 0x4549b5 e8a681ffff CALL runtime.morestack_noctxt(SB)
defer 關鍵字首先會調用 runtime.deferproc 定義一個延遲調用對象,然後再函數結束前,調用 runtime.deferreturn 來完成 defer 定義的函數的調用
panic 函數就會調用 runtime.gopanic 來實現相關的邏輯
recover 則調用 runtime.gorecover 來實現 recover 的功能
deferproc
根據 defer 關鍵字後面定義的函數 fn 以及 參數的size,來創建一個延遲執行的 函數,並將這個延遲函數,掛在到當前g的 _defer 的鏈表上,下面是deferproc的實現:
1 func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
2 sp := getcallersp()
3 argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
4 callerpc := getcallerpc()
5 // 獲取一個_defer對象, 並放入g._defer鏈表的頭部
6 d := newdefer(siz)
7 // 設置defer的fn pc sp等,後面調用
8 d.fn = fn
9 d.pc = callerpc
10 d.sp = sp
11 switch siz {
12 case 0:
13 // Do nothing.
14 case sys.PtrSize:
15 // _defer 後面的內存 存儲 argp的地址信息
16 *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
17 default:
18 // 如果不是指針類型的參數,把參數拷貝到 _defer 的後面的內存空間
19 memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
20 }
21 return0()
22 }
通過newproc 獲取一個 _defer 的對象,並加入到當前g的 _defer 鏈表的頭部,然後再把參數或參數的指針拷貝到 獲取到的 _defer對象的後面的內存空間。
再看看newdefer 的實現:
1 func newdefer(siz int32) *_defer {
2 var d *_defer
3 // 根據 size 通過deferclass判斷應該分配的 sizeclass,就類似於 內存分配預先確定好幾個sizeclass,然後根據size確定sizeclass,找對應的緩存的內存塊
4 sc := deferclass(uintptr(siz))
5 gp := getg()
6 // 如果sizeclass在既定的sizeclass範圍內,去g綁定的p上找
7 if sc < uintptr(len(p{}.deferpool)) {
8 pp := gp.m.p.ptr()
9 if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
10 // 當前sizeclass的緩存數量==0,且不為nil,從sched上獲取一批緩存
11 systemstack(func() {
12 lock(&sched.deferlock)
13 for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
14 d := sched.deferpool[sc]
15 sched.deferpool[sc] = d.link
16 d.link = nil
17 pp.deferpool[sc] = append(pp.deferpool[sc], d)
18 }
19 unlock(&sched.deferlock)
20 })
21 }
22 // 如果從sched獲取之後,sizeclass對應的緩存不為空,分配
23 if n := len(pp.deferpool[sc]); n > 0 {
24 d = pp.deferpool[sc][n-1]
25 pp.deferpool[sc][n-1] = nil
26 pp.deferpool[sc] = pp.deferpool[sc][:n-1]
27 }
28 }
29 // p和sched都沒有找到 或者 沒有對應的sizeclass,直接分配
30 if d == nil {
31 // Allocate new defer+args.
32 systemstack(func() {
33 total := roundupsize(totaldefersize(uintptr(siz)))
34 d = (*_defer)(mallocgc(total, deferType, true))
35 })
36 }
37 d.siz = siz
38 // 插入到g._defer的鏈表頭
39 d.link = gp._defer
40 gp._defer = d
41 return d
42 }
newdefer的作用是獲取一個_defer對象, 並推入 g._defer鏈表的頭部。根據size獲取sizeclass,對sizeclass進行分類緩存,這是內存分配時的思想,先去p上分配,然後批量從全局 sched上獲取到本地緩存,這種二級緩存的思想真的在go源碼的各個部分都有。
deferreturn
1 func deferreturn(arg0 uintptr) {
2 gp := getg()
3 // 獲取g defer鏈表的第一個defer,也是最後一個聲明的defer
4 d := gp._defer
5 // 沒有defer,就不需要干什麼事了
6 if d == nil {
7 return
8 }
9 sp := getcallersp()
10 // 如果defer的sp與callersp不匹配,說明defer不對應,有可能是調用了其他棧幀的延遲函數
11 if d.sp != sp {
12 return
13 }
14 // 根據d.siz,把原先存儲的參數信息獲取並存儲到arg0裏面
15 switch d.siz {
16 case 0:
17 // Do nothing.
18 case sys.PtrSize:
19 *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
20 default:
21 memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
22 }
23 fn := d.fn
24 d.fn = nil
25 // defer用過了就釋放了,
26 gp._defer = d.link
27 freedefer(d)
28 // 跳轉到執行defer
29 jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
30 }
freedefer
釋放defer用到的函數,應該跟調度器、內存分配的思想是一樣的。
1 func freedefer(d *_defer) {
2 // 判斷defer的sizeclass
3 sc := deferclass(uintptr(d.siz))
4 // 超出既定的sizeclass範圍的話,就是直接分配的內存,那就不管了
5 if sc >= uintptr(len(p{}.deferpool)) {
6 return
7 }
8 pp := getg().m.p.ptr()
9 // p本地sizeclass對應的緩衝區滿了,批量轉移一半到全局sched
10 if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
11 // 使用g0來轉移
12 systemstack(func() {
13 var first, last *_defer
14 for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
15 n := len(pp.deferpool[sc])
16 d := pp.deferpool[sc][n-1]
17 pp.deferpool[sc][n-1] = nil
18 pp.deferpool[sc] = pp.deferpool[sc][:n-1]
19 // 先將需要轉移的那批defer對象串成一個鏈表
20 if first == nil {
21 first = d
22 } else {
23 last.link = d
24 }
25 last = d
26 }
27 lock(&sched.deferlock)
28 // 把這個鏈表放到sched.deferpool對應sizeclass的鏈表頭
29 last.link = sched.deferpool[sc]
30 sched.deferpool[sc] = first
31 unlock(&sched.deferlock)
32 })
33 }
34 // 清空當前要釋放的defer的屬性
35 d.siz = 0
36 d.started = false
37 d.sp = 0
38 d.pc = 0
39 d.link = nil
40
41 pp.deferpool[sc] = append(pp.deferpool[sc], d)
42 }
gopanic
1 func gopanic(e interface{}) {
2 gp := getg()
3
4 var p _panic
5 p.arg = e
6 p.link = gp._panic
7 gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
8
9 atomic.Xadd(&runningPanicDefers, 1)
10 // 依次執行 g._defer鏈表的defer對象
11 for {
12 d := gp._defer
13 if d == nil {
14 break
15 }
16
17 // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
18 // take defer off list. The earlier panic or Goexit will not continue running.
19 // 正常情況下,defer執行完成之後都會被移除,既然這個defer沒有移除,原因只有兩種: 1. 這個defer裏面引發了panic 2. 這個defer裏面引發了 runtime.Goexit,但是這個defer已經執行過了,需要移除,如果引發這個defer沒有被移除是第一個原因,那麼這個panic也需要移除,因為這個panic也執行過了,這裏給panic增加標誌位,以待後續移除
20 if d.started {
21 if d._panic != nil {
22 d._panic.aborted = true
23 }
24 d._panic = nil
25 d.fn = nil
26 gp._defer = d.link
27 freedefer(d)
28 continue
29 }
30 d.started = true
31
32 // Record the panic that is running the defer.
33 // If there is a new panic during the deferred call, that panic
34 // will find d in the list and will mark d._panic (this panic) aborted.
35 // 把當前的panic 綁定到這個defer上面,defer裏面有可能panic,這種情況下就會進入到 上面d.started 的邏輯裏面,然後把當前的panic終止掉,因為已經執行過了
36 d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
37 // 執行defer.fn
38 p.argp = unsafe.Pointer(getargp(0))
39 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
40 p.argp = nil
41
42 // reflectcall did not panic. Remove d.
43 if gp._defer != d {
44 throw("bad defer entry in panic")
45 }
46 // 解決defer與panic的綁定關係,因為 defer函數已經執行完了,如果有panic或Goexit就不會執行到這裏了
47 d._panic = nil
48 d.fn = nil
49 gp._defer = d.link
50
51 // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
52 //GC()
53
54 pc := d.pc
55 sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
56 freedefer(d)
57 // panic被recover了,就不需要繼續panic了,繼續執行剩餘的代碼
58 if p.recovered {
59 atomic.Xadd(&runningPanicDefers, -1)
60
61 gp._panic = p.link
62 // Aborted panics are marked but remain on the g.panic list.
63 // Remove them from the list.
64 // 從panic鏈表中移除aborted的panic,下面解釋
65 for gp._panic != nil && gp._panic.aborted {
66 gp._panic = gp._panic.link
67 }
68 if gp._panic == nil { // must be done with signal
69 gp.sig = 0
70 }
71 // Pass information about recovering frame to recovery.
72 gp.sigcode0 = uintptr(sp)
73 gp.sigcode1 = pc
74 // 調用recovery, 恢復當前g的調度執行
75 mcall(recovery)
76 throw("recovery failed") // mcall should not return
77 }
78 }
79 // 打印panic信息
80 preprintpanics(gp._panic)
81 // panic
82 fatalpanic(gp._panic) // should not return
83 *(*int)(nil) = 0 // not reached
84 }
看下裏面gp._panic.aborted 的作用:
1 func main() {
2 defer func() { // defer1
3 recover()
4 }()
5 panic1()
6 }
7
8 func panic1() {
9 defer func() { // defer2
10 panic("error1") // panic2
11 }()
12 panic("error") // panic1
13 }
執行順序詳解:
g._defer鏈表: g._defer->defer2->defer1
g._panic鏈表:g._panic->panic1
g._defer鏈表: g._defer->defer2->defer1
g._panic鏈表:g._panic->panic2->panic1
- 繼續執行到 defer1 函數內部,進行recover()
此時會去恢復 panic2 引起的 panic, panic2.recovered = true,應該順着g._panic鏈表繼續處理下一個panic了,但是我們可以發現 panic1 已經執行過了,這也就是下面的代碼的邏輯了,去掉已經執行過的panic
1 for gp._panic != nil && gp._panic.aborted {
2 gp._panic = gp._panic.link
3 }
panic的邏輯:
程序在遇到panic的時候,就不再繼續執行下去了,先把當前panic 掛載到 g._panic 鏈表上,開始遍歷當前g的g._defer鏈表,然後執行_defer對象定義的函數等,如果 defer函數在調用過程中又發生了 panic,則又執行到了 gopanic函數,最後,循環打印所有panic的信息,並退出當前g。然而,如果調用defer的過程中,遇到了recover,則繼續進行調度(mcall(recovery))。
recovery
1 func recovery(gp *g) {
2 // Info about defer passed in G struct.
3 sp := gp.sigcode0
4 pc := gp.sigcode1
5 // Make the deferproc for this d return again,
6 // this time returning 1. The calling function will
7 // jump to the standard return epilogue.
8 // 記錄defer返回的sp pc
9 gp.sched.sp = sp
10 gp.sched.pc = pc
11 gp.sched.lr = 0
12 gp.sched.ret = 1
13 // 重新恢復執行調度
14 gogo(&gp.sched)
15 }
gorecover
gorecovery 僅僅只是設置了 g._panic.recovered 的標誌位
1 func gorecover(argp uintptr) interface{} {
2 gp := getg()
3 p := gp._panic
4 // 需要根據 argp的地址,判斷是否在defer函數中被調用
5 if p != nil && !p.recovered && argp == uintptr(p.argp) {
6 // 設置標誌位,上面gopanic中會對這個標誌位做判斷
7 p.recovered = true
8 return p.arg
9 }
10 return nil
11 }
goexit
當手動調用 runtime.Goexit() 退出的時候,defer函數也會執行:
1 func Goexit() {
2 // Run all deferred functions for the current goroutine.
3 // This code is similar to gopanic, see that implementation
4 // for detailed comments.
5 gp := getg()
6 // 遍歷defer鏈表
7 for {
8 d := gp._defer
9 if d == nil {
10 break
11 }
12 // 如果 defer已經執行過了,與defer綁定的panic 終止掉
13 if d.started {
14 if d._panic != nil {
15 d._panic.aborted = true
16 d._panic = nil
17 }
18 d.fn = nil
19 // 從defer鏈表中移除
20 gp._defer = d.link
21 // 釋放defer
22 freedefer(d)
23 continue
24 }
25 // 調用defer內部函數
26 d.started = true
27 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
28 if gp._defer != d {
29 throw("bad defer entry in Goexit")
30 }
31 d._panic = nil
32 d.fn = nil
33 gp._defer = d.link
34 freedefer(d)
35 // Note: we ignore recovers here because Goexit isn't a panic
36 }
37 // 調用goexit0,清除當前g的屬性,重新進入調度
38 goexit1()
39 }
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能
※評比前十大台北網頁設計、台北網站設計公司知名案例作品心得分享
※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選