go中的關鍵字-defer
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 }
執行順序詳解:
- 當執行到
panic("error")
時
g._defer連結串列: g._defer->defer2->defer1
g._panic連結串列:g._panic->panic1
- 當執行到
panic("error1")
時
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 }