defer是golang提供的關鍵字,在函數或方法執行完成,並返回之前呼叫。
每次defer都會將defer函數壓入堆疊中,呼叫函數或方法結束時,從堆疊中取出執行,所以多個defer的執行順序是先入後出。
for i := 0; i <= 3; i++ { defer fmt.Print(i) } //输出结果时 3,2,1,0
defer的觸發時機
官網說的很清楚:
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
- defer #包裹著defer語句的函數執行到最後時
當前goroutine發生Panic時
//输出结果:return前执行defer func f1() { defer fmt.Println("return前执行defer") return } //输出结果:函数执行 // 函数执行到最后 func f2() { defer fmt.Println("函数执行到最后") fmt.Println("函数执行") } //输出结果:panic前 第一个defer在Panic发生时执行,第二个defer在Panic之后声明,不能执行到 func f3() { defer fmt.Println("panic前") panic("panic中") defer fmt.Println("panic后") }
defer,return,傳回值的執行順序
先來看3個範例
func f1() int { //匿名返回值 var r int = 6 defer func() { r *= 7 }() return r } func f2() (r int) { //有名返回值 defer func() { r *= 7 }() return 6 } func f3() (r int) { //有名返回值 defer func(r int) { r *= 7 }(r) return 6 }
f1的執行結果是6, f2的執行結果是42,f3的執行結果是6
在golang的官方文檔裡面介紹了,return,defer,回傳值的執行順序:
if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return state state turn state statement .
1. 先給回傳值賦值
2.執行defer語句
3. 包裹函數return回傳
#f1的結果是6。 f1是匿名回傳值,匿名回傳值是在return執行時被聲明,因此defer宣告時,還不能存取到匿名回傳值,defer的修改不會影響到回傳值。
f2先給回傳值r賦值,r=6,執行defer語句,defer修改r, r = 42,然後函數return。
f3是有名回傳值,但因為r是作為defer的傳參,在宣告defer的時候,就進行參數拷貝傳遞,所以defer只會對defer函數的局部參數有影響,不會影響到呼叫函數的回傳值。
閉包與匿名函數
匿名函數:沒有函數名稱的函數。
閉包:可以使用另外一個函數作用域中的變數的函數。
for i := 0; i <= 3; i++ { defer func() { fmt.Print(i) } } //输出结果时 3,3,3,3 因为defer函数的i是对for循环i的引用,defer延迟执行,for循环到最后i是3,到defer执行时i就 是3 for i := 0; i <= 3; i++ { defer func(i int) { fmt.Print(i) }(i) } //输出结果时 3,2,1,0 因为defer函数的i是在defer声明的时候,就当作defer参数传递到defer函数中
defer原始碼解析
defer的實作原始碼是在runtime.deferproc
然後在函數傳回之前的地方,執行函數runtime.deferreturn。
先了解defer結構體:
type _defer struct { siz int32 started bool sp uintptr // sp at time of defer pc uintptr fn *funcval _panic *_panic // panic that is running defer link *_defer }
sp 和pc 分別指向了堆疊指標和呼叫方的程式計數器,fn是向defer 關鍵字中傳入的函數,Panic是導致運行defer的Panic 。
每遇到一個defer關鍵字,defer函數都會被轉換成runtime.deferproc
deferproc透過newdefer建立延遲函數,並將這個新建的延遲函數掛在目前goroutine的_defer的鍊錶上
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() }
newdefer會先從sched和目前p的deferpool中取出一個_defer結構體,如果deferpool沒有_defer,則初始化一個新的_defer。
_defer是關聯到目前的g,所以defer只對目前g有效。
d.link = gp._defer
gp._defer = d //用鍊錶連接目前g的所有defer
func newdefer(siz int32) *_defer { var d *_defer sc := deferclass(uintptr(siz)) gp := getg() if sc < uintptr(len(p{}.deferpool)) { pp := gp.m.p.ptr() if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { ..... d := sched.deferpool[sc] sched.deferpool[sc] = d.link d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) } if n := len(pp.deferpool[sc]); n > 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } ...... d.siz = siz d.link = gp._defer gp._defer = d return d }
deferreturn 從目前g取出_defer鍊錶執行,每個_defer調用freedefer釋放_defer結構體,並將該_defer結構體放入目前p的deferpool中。
defer效能分析
defer在開發中,對於資源的釋放,捕獲Panic等很有用處。可以有些開發者沒有考慮過defer對程式效能的影響,在程式中濫用defer。
在效能測試中可以發現,defer對效能還是有一些影響。雨痕的Go 效能最佳化技巧 4/1,對defer語句帶來的額外開銷有一些測試。
測試程式碼
var mu sync.Mutex func noDeferLock() { mu.Lock() mu.Unlock() } func deferLock() { mu.Lock() defer mu.Unlock() } func BenchmarkNoDefer(b *testing.B) { for i := 0; i < b.N; i++ { noDeferLock() } } func BenchmarkDefer(b *testing.B) { for i := 0; i < b.N; i++ { deferLock() }
測試結果:
BenchmarkNoDefer-4 100000000 11.1 ns/op BenchmarkDefer-4 36367237 33.1 ns/op
透過前面的原始碼解析可以知道,defer會先呼叫deferproc ,這些都會進行參數拷貝,deferreturn也會提取相關資訊延遲執行,這些都是比直接call一條語句消耗更大。
defer效能不高,每次defer耗時20ns,,在一個func內連續出現多次,效能消耗是20ns*n,累計出來浪費的cpu資源很大的。
解決之道:除了需要異常捕獲時,必須使用defer;其它資源回收類別defer,可以判斷失敗後,使用goto跳到資源回收的程式碼區。對於競爭資源,可以在使用完之後,立刻釋放資源,這樣才能最優的使用競爭資源。