深入理解golang的defer
defer 估計是每個 Gopher 每天寫代碼都會寫,那么你是不是真正的理解了 defer 呢?不妨看一下下面這個代碼片段,這個是我之前給 UC 那邊一個 team 做 Golang 培訓的時候想的例子。
package mainfunc f() int {i := 5defer func() {i++}()return i }func f1() (result int) { defer func() { result++ }() return 0 }func f2() (r int) { t := 5 defer func() { t = t + 5 }()return t }func f3() (r int) { defer func(r int) { r = r + 5 }(r) return 1 }func main() {println(f())println(f1())println(f2())println(f3()) }1. return 語句
在解析上面的題目之前,要理解一個前提是 Go 的函數返回值是通過堆棧返回的,這也是實現了多返回值的方法。舉個例子。
//foo.go package mainfunc foo() (int,int){i := 1j := 2return i,j }func main() {foo() }查看匯編代碼如下。
$ go build -gcflags '-l' -o foo foo.go $ go tool objdump -s "main\.foo" foo TEXT main.foo(SB) /Users/kltao/code/go/src/example/foo.gobar.go:6 0x104ea70 48c744240801000000 MOVQ $0x1, 0x8(SP)bar.go:6 0x104ea79 48c744241002000000 MOVQ $0x2, 0x10(SP)bar.go:6 0x104ea82 c3 RET也就是說 return 語句不是原子操作,而是被拆成了兩步
rval = xxx ret而 defer 語句就是在這兩條語句之間執行,也就是
rval = xxx defer_func ret另外在 Go 語言的 func 聲明中如果返回值變量顯示聲明,也就是?func foo() (ret int) {}?的時候,rval 就是 ret。這么上面的題目中對于的函數執行簡單來說就是如下代碼片段。但是 f3 涉及到另外一個知識點,也就是閉包。
//f rval = i i ++ ret//f1 result = 0 defer // result ++ return//f2 r = t defer // t = t + 5 return2. 閉包
簡單來說,Go 語言中的閉包就是在函數內引用函數體之外的數據,這樣就會產生一種結果,雖然數據定義是在函數外,但是在函數內部操作數據也會對數據產生影響。如下面的例子所示,foo() 中的匿名函數對 i 的調用就是閉包引用,i++ 會影響外面定義的 i 的值。而 bar() 中的匿名函數是變量拷貝,i++ 并不會修改外部 i 值。這么看的話,開始的 f3() 的輸出你是不是知道是多少了呢?
func foo() {i := 1go func() {i ++ }()time.Sleep(xxx)println(i) }func bar() {i := 1go func(i int) {i ++}(i)time.Sleep(xxx)println(i) }3. defer 的使用場景
在我最開始學習 Go 語言的時候,我看到 defer 的第一反應就是 Python 中的如下語句。也就是說不用顯示地關閉文件句柄,除此之外還有網絡連接等各種資源都可以放到 defer 里面來釋放。
with open("file", "a") as f:// handler但是隨著寫代碼越來越多,我覺得上面說的這些場景如果明確知道什么時候要釋放資源,那么都不是非使用 defer 不可的,因為使用 defer 還是有很大開銷的,下面說。使用 defer 的最合適的場景我覺得應該是和 recover 結合使用,也就是說在你不知道的程序何時可能會 panic 的時候,才引入 defer + recover。
func f() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in f", r)}}() }4. defer 的底層實現
defer 的底層實現主要由兩個函數:
- func deferproc(siz int32, fn *funcval)
- func deferreturn(arg0 uintptr)
看代碼。下面的代碼執行了兩次 defer ,defer 的執行是按 FILO 的次序執行的,也就是說下面代碼的輸出是
world hello2 hello1這個就不細說了??磪R編代碼。
package mainimport ("fmt" )func main() {defer fmt.Println("hello1")defer fmt.Println("hello2")fmt.Println("world") }編譯,objdump。
$ go build -gcflags '-l' -o defer defer.go $ go tool objdump -s "main\.main" defer TEXT main.main(SB) /Users/kltao/code/go/src/example/defer2.go...defer2.go:8 0x1092fe1 0f57c0 XORPS X0, X0defer2.go:8 0x1092fe4 0f11442450 MOVUPS X0, 0x50(SP)defer2.go:8 0x1092fe9 488d05100c0100 LEAQ type.*+68224(SB), AXdefer2.go:8 0x1092ff0 4889442450 MOVQ AX, 0x50(SP)defer2.go:8 0x1092ff5 488d0db4b00400 LEAQ main.statictmp_0(SB), CXdefer2.go:8 0x1092ffc 48894c2458 MOVQ CX, 0x58(SP)defer2.go:8 0x1093001 c7042430000000 MOVL $0x30, 0(SP)defer2.go:8 0x1093008 488d0d999d0300 LEAQ go.func.*+8(SB), CXdefer2.go:8 0x109300f 48894c2408 MOVQ CX, 0x8(SP)defer2.go:8 0x1093014 488d542450 LEAQ 0x50(SP), DXdefer2.go:8 0x1093019 4889542410 MOVQ DX, 0x10(SP)defer2.go:8 0x109301e 48c744241801000000 MOVQ $0x1, 0x18(SP)defer2.go:8 0x1093027 48c744242001000000 MOVQ $0x1, 0x20(SP)defer2.go:8 0x1093030 e81b3bf9ff CALL runtime.deferproc(SB)defer2.go:8 0x1093035 85c0 TESTL AX, AXdefer2.go:8 0x1093037 0f85b8000000 JNE 0x10930f5defer2.go:9 0x109303d 0f57c0 XORPS X0, X0defer2.go:9 0x1093040 0f11442440 MOVUPS X0, 0x40(SP)defer2.go:9 0x1093045 488d05b40b0100 LEAQ type.*+68224(SB), AXdefer2.go:9 0x109304c 4889442440 MOVQ AX, 0x40(SP)defer2.go:9 0x1093051 488d0d68b00400 LEAQ main.statictmp_1(SB), CXdefer2.go:9 0x1093058 48894c2448 MOVQ CX, 0x48(SP)defer2.go:9 0x109305d c7042430000000 MOVL $0x30, 0(SP)defer2.go:9 0x1093064 488d0d3d9d0300 LEAQ go.func.*+8(SB), CXdefer2.go:9 0x109306b 48894c2408 MOVQ CX, 0x8(SP)defer2.go:9 0x1093070 488d4c2440 LEAQ 0x40(SP), CXdefer2.go:9 0x1093075 48894c2410 MOVQ CX, 0x10(SP)defer2.go:9 0x109307a 48c744241801000000 MOVQ $0x1, 0x18(SP)defer2.go:9 0x1093083 48c744242001000000 MOVQ $0x1, 0x20(SP)defer2.go:9 0x109308c e8bf3af9ff CALL runtime.deferproc(SB)defer2.go:9 0x1093091 85c0 TESTL AX, AXdefer2.go:9 0x1093093 7550 JNE 0x10930e5defer2.go:11 0x1093095 0f57c0 XORPS X0, X0defer2.go:11 0x1093098 0f11442460 MOVUPS X0, 0x60(SP)defer2.go:11 0x109309d 488d055c0b0100 LEAQ type.*+68224(SB), AXdefer2.go:11 0x10930a4 4889442460 MOVQ AX, 0x60(SP)defer2.go:11 0x10930a9 488d0520b00400 LEAQ main.statictmp_2(SB), AXdefer2.go:11 0x10930b0 4889442468 MOVQ AX, 0x68(SP)defer2.go:11 0x10930b5 488d442460 LEAQ 0x60(SP), AXdefer2.go:11 0x10930ba 48890424 MOVQ AX, 0(SP)defer2.go:11 0x10930be 48c744240801000000 MOVQ $0x1, 0x8(SP)defer2.go:11 0x10930c7 48c744241001000000 MOVQ $0x1, 0x10(SP)defer2.go:11 0x10930d0 e80b99ffff CALL fmt.Println(SB)defer2.go:12 0x10930d5 90 NOPLdefer2.go:12 0x10930d6 e80543f9ff CALL runtime.deferreturn(SB)defer2.go:12 0x10930db 488b6c2470 MOVQ 0x70(SP), BPdefer2.go:12 0x10930e0 4883c478 ADDQ $0x78, SPdefer2.go:12 0x10930e4 c3 RETdefer2.go:9 0x10930e5 90 NOPLdefer2.go:9 0x10930e6 e8f542f9ff CALL runtime.deferreturn(SB)defer2.go:9 0x10930eb 488b6c2470 MOVQ 0x70(SP), BPdefer2.go:9 0x10930f0 4883c478 ADDQ $0x78, SPdefer2.go:9 0x10930f4 c3 RET...結合代碼看,代碼中使用了兩次 defer,調用了 deferproc 和 deferreturn ,都是匹配成對調用的。我們看一下 Golang 源碼里面對 deferproc 和 deferreturn 的實現。
// Create a new deferred function fn with siz bytes of arguments. // The compiler turns a defer statement into a call to this. //go:nosplit func deferproc(siz int32, fn *funcval) { // arguments of fn follow fnif getg().m.curg != getg() { // getg 是獲取當前的 goroutine// go code on the system stack can't deferthrow("defer on system stack")}// the arguments of fn are in a perilous state. The stack map// for deferproc does not describe them. So we can't let garbage// collection or stack copying trigger until we've copied them out// to somewhere safe. The memmove below does that.// Until the copy completes, we can only call nosplit routines.sp := getcallersp()argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)callerpc := getcallerpc()d := newdefer(siz) // 申請一個結構體用來存放 defer 相關數據if d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}d.fn = fnd.pc = callerpcd.sp = spswitch siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))default:memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))}// deferproc returns 0 normally.// a deferred func that stops a panic// makes the deferproc return 1.// the code the compiler generates always// checks the return value and jumps to the// end of the function if deferproc returns != 0.return0()// No code can go here - the C return register has// been set and must not be clobbered. }光看 deferproc 的代碼只能看到一個申請 defer 對象的過程,并沒有看到這個 defer 對象存儲在哪里?那么不妨大膽設想一下,defer 對象是以鏈表的形式關聯到 goroutine 上的。我們看一下 deferproc 中調用的 newdefer 函數。
func newdefer(siz int32) *_defer {var d *_defersc := 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 {// Take the slow path on the system stack so// we don't grow newdefer's stack.systemstack(func() {lock(&sched.deferlock)for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {d := sched.deferpool[sc]sched.deferpool[sc] = d.linkd.link = nilpp.deferpool[sc] = append(pp.deferpool[sc], d)}unlock(&sched.deferlock)})}if n := len(pp.deferpool[sc]); n > 0 {d = pp.deferpool[sc][n-1]pp.deferpool[sc][n-1] = nilpp.deferpool[sc] = pp.deferpool[sc][:n-1]}}if d == nil {// Allocate new defer+args.systemstack(func() {total := roundupsize(totaldefersize(uintptr(siz)))d = (*_defer)(mallocgc(total, deferType, true))})if debugCachedWork {// Duplicate the tail below so if there's a// crash in checkPut we can tell if d was just// allocated or came from the pool.d.siz = sizd.link = gp._defergp._defer = dreturn d}}d.siz = sizd.link = gp._defergp._defer = dreturn d }重點看第 44,45 行,gp 是當前的 goroutine,有一個字段 _defer 是用來存放 defer 結構的,然后我們發現 defer 結構有一個 link 字段其實就相當于鏈表指針。如果熟悉鏈表操作的話,第 44,45 行結合起來看就是將新的 defer 對象插入到 goroutine 關聯的 defer 鏈表的頭部。那么執行的時候就從頭執行 defer 就是 FILO 的順序了,deferreturn 的源碼大家自己去看吧。
5. benchmark
看了第 4 部分,我們應該知道 defer 的調用開銷相比直接的函數調用確實多了不少,那么有沒有 benchmark 來直觀的看一下呢?有的。這里使用雨痕的 《Go 語言學習筆記》的 benchmark 程序。
package mainimport ("testing""sync" )var m sync.Mutexfunc call() {m.Lock()m.Unlock() }func deferCall() {m.Lock()defer m.Unlock() }func BenchmarkCall(b *testing.B) {for i:=0; i<b.N; i++ {call()} }func BenchmarkDeferCall(b *testing.B) {for i:=0; i<b.N; i++ {deferCall()} }測試結果如下,看的出來差距還是挺大的。
? df go test -bench=. goos: darwin goarch: amd64 pkg: example/df BenchmarkCall-8 100000000 17.8 ns/op BenchmarkDeferCall-8 20000000 56.3 ns/op6. 參考
最后,我之前只在博客?http://www.legendtkl.com?和知乎上(知乎專欄:Golang Inside)上面寫文章,現在開始在公眾號(公眾號:legendtkl)上面嘗試一下,如果你覺得不錯,或者之前看過,歡迎關注或者推薦給身邊的人。謝謝。
總結
以上是生活随笔為你收集整理的深入理解golang的defer的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: golang处理kill命令总结
- 下一篇: redis的事务总结