一篇短文介紹 Defer 是如何工作的

?? 這篇文章基于 Go 1.12。
`defer` 語(yǔ)句[1]是在函數(shù)返回前執(zhí)行一段代碼的便捷方法,如 Golang 規(guī)范[2]所描述:
延遲函數(shù)( deferred functions )在所在函數(shù)返回前,以與聲明相反的順序立即被調(diào)用
以下是 LIFO (后進(jìn)先出)實(shí)現(xiàn)的例子:
func?main()?{
???defer?func()?{
??????println(`defer?1`)
???}()
???defer?func()?{
??????println(`defer?2`)
???}()
}
defer?2?<-?后進(jìn)先出
defer?1
來(lái)看一下內(nèi)部的實(shí)現(xiàn),然后再看一個(gè)更復(fù)雜的案例。
內(nèi)部實(shí)現(xiàn)
Go 運(yùn)行時(shí)(runtime)使用一個(gè)鏈表來(lái)實(shí)現(xiàn) LIFO。實(shí)際上,一個(gè) defer 結(jié)構(gòu)體持有一個(gè)指向下一個(gè)要被執(zhí)行的 defer 結(jié)構(gòu)體的指針:
type?_defer?struct?{
???siz?????int32
???started?bool
???sp??????uintptr
???pc??????uintptr
???fn??????*funcval
???_panic??*_panic
???link????*_defer?//?下一個(gè)要被執(zhí)行的延遲函數(shù)
當(dāng)一個(gè)新的 defer 方法被創(chuàng)建的時(shí)候,它被附加到當(dāng)前的 Goroutine 上,然后之前的 defer 方法作為下一個(gè)要執(zhí)行的函數(shù)被鏈接到新創(chuàng)建的方法上:
func?newdefer(siz?int32)?*_defer?{
???var?d?*_defer
???gp?:=?getg()?//?獲取當(dāng)前?goroutine
???[...]
???//?延遲列表現(xiàn)在被附加到新的?_defer?結(jié)構(gòu)體
???d.link?=?gp._defer
???gp._defer?=?d?//?新的結(jié)構(gòu)現(xiàn)在是第一個(gè)被調(diào)用的
???return?d
}
現(xiàn)在,后續(xù)調(diào)用會(huì)從棧的頂部依次出棧延遲函數(shù):
func?deferreturn(arg0?uintptr)?{
???gp?:=?getg()?//?獲取當(dāng)前?goroutine
???d:=?gp._defer?//?拷貝延遲函數(shù)到一個(gè)變量上
???if?d?==?nil?{?//?如果不存在延遲函數(shù)就直接返回
??????return
???}
???[...]
???fn?:=?d.fn?//?獲取要調(diào)用的函數(shù)
???d.fn?=?nil?//?重置函數(shù)
???gp._defer?=?d.link?//?把下一個(gè)?_defer?結(jié)構(gòu)體依附到?Goroutine?上
???freedefer(d)?//?釋放?_defer?結(jié)構(gòu)體
???jmpdefer(fn,?uintptr(unsafe.Pointer(&arg0)))?//?調(diào)用該函數(shù)
}
如我們所見(jiàn),并沒(méi)有循環(huán)地去調(diào)用延遲函數(shù),而是一個(gè)接一個(gè)地出棧。這一行為可以通過(guò)生成匯編[3]代碼得到驗(yàn)證:
// 第一個(gè)延遲函數(shù)
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
// 第二個(gè)延遲函數(shù)
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
// main 函數(shù)結(jié)束
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 方法被調(diào)用了兩次,并且內(nèi)部調(diào)用了 newdefer 方法,我們之前已經(jīng)看到該方法將我們的函數(shù)注冊(cè)為延遲函數(shù)。之后,在函數(shù)的最后,在 deferreturn 函數(shù)的幫助下,延遲方法會(huì)被一個(gè)接一個(gè)地調(diào)用。
Go 標(biāo)準(zhǔn)庫(kù)向我們展示了結(jié)構(gòu)體 _defer 同樣鏈接了一個(gè) _panic *_panic 屬性。來(lái)通過(guò)另一個(gè)例子看下它在哪里會(huì)起作用。
延遲和返回值
如規(guī)范所描述,延遲函數(shù)訪問(wèn)返回的結(jié)果的唯一方法是使用命名返回參數(shù)[4]:
如果延遲函數(shù)是一個(gè)匿名函數(shù)( function literal )[5],并且所在函數(shù)存在命名返回參數(shù)[6],同時(shí)該命名返回參數(shù)在匿名函數(shù)的作用域中,匿名函數(shù)可能會(huì)在返回參數(shù)返回前訪問(wèn)并修改它們。
這里有個(gè)例子:
func?main()?{
???fmt.Printf("with?named?param,?x:?%d\n",?namedParam())
???fmt.Printf("without?named?param,?x:?%d\n",?notNamedParam())
}
func?namedParam()?(x?int)?{
???x?=?1
???defer?func()?{?x?=?2?}()
???return?x
}
func?notNamedParam()?(int)?{
???x?:=?1
???defer?func()?{?x?=?2?}()
???return?x
}
with?named?param,?x:?2
without?named?param,?x:?1
確實(shí)就像這篇“defer, panic 和 recover[7]”博客所描述的一樣,一旦確定這一行為,我們可以將其與 recover 函數(shù)混合使用:
recover 函數(shù) 是一個(gè)用于重新獲取對(duì)恐慌(panicking)goroutine 控制的內(nèi)置函數(shù)。recover 函數(shù)僅在延遲函數(shù)內(nèi)部時(shí)才有效。
如我們所見(jiàn),_defer 結(jié)構(gòu)體鏈接了一個(gè) _panic 屬性,該屬性在 panic 調(diào)用期間被鏈接。
func?gopanic(e?interface{})?{
???[...]
???var?p?_panic
???[...]
???d?:=?gp._defer?//?當(dāng)前附加的?defer?函數(shù)
???[...]
???d._panic?=?(*_panic)(noescape(unsafe.Pointer(&p)))
???[...]
}
確實(shí),在發(fā)生 panic 的情況下,調(diào)用延遲函數(shù)之前會(huì)調(diào)用 gopanic 方法:
0x0067 00103 (main.go:21) CALL runtime.gopanic(SB)
0x006c 00108 (main.go:21) UNDEF
0x006e 00110 (main.go:16) XCHGL AX, AX
0x006f 00111 (main.go:16) CALL runtime.deferreturn(SB)
這里是一個(gè) recover 函數(shù)利用命名返回參數(shù)的例子:
func?main()?{
???fmt.Printf("error?from?err1:?%v\n",?err1())
???fmt.Printf("error?from?err2:?%v\n",?err2())
}
func?err1()?error?{
???var?err?error
???defer?func()?{
??????if?r?:=?recover();?r?!=?nil?{
?????????err?=?errors.New("recovered")
??????}
???}()
???panic(`foo`)
???return?err
}
func?err2()?(err?error)?{
???defer?func()?{
??????if?r?:=?recover();?r?!=?nil?{
?????????err?=?errors.New("recovered")
??????}
???}()
???panic(`foo`)
???return?err
}
error?from?err1:?<nil>
error?from?err2:?recovered
兩者的結(jié)合是我們可以正常使用 recover 函數(shù)將我們希望的 error 返回給調(diào)用方。作為這篇關(guān)于延遲函數(shù)的文章的總結(jié),讓我們來(lái)看看延遲函數(shù)的提升。
性能提升
Go 1.8[8]是提升 defer 的最近的一個(gè)版本(譯者注:目前 Go 1.14 才是提升 defer 性能的最近的一個(gè)版本),我們可以通過(guò)運(yùn)行 Go 的基準(zhǔn)測(cè)試來(lái)看到這些提升(在 1.7 和 1.8 之間進(jìn)行對(duì)比):
name?????????old?time/op??new?time/op??delta
Defer-4??????99.0ns?±?9%??52.4ns?±?5%??-47.04%??(p=0.000?n=9+10)
Defer10-4????90.6ns?±?13%??45.0ns?±?3%??-50.37%??(p=0.000?n=10+10)
這樣的提升得益于這個(gè)提升分配方式的 CL [9],避免了棧的增長(zhǎng)。
不帶參數(shù)的 defer 語(yǔ)句避免內(nèi)存拷貝也是一個(gè)優(yōu)化。下面是帶參數(shù)和不帶參數(shù)的延遲函數(shù)的基準(zhǔn)測(cè)試:
name?????old?time/op??new?time/op??delta
Defer-4??51.3ns?±?3%??45.8ns?±?1%??-10.72%??(p=0.000?n=10+10)
由于第二個(gè)優(yōu)化,現(xiàn)在速度也提高了 10%。
via: https://medium.com/a-journey-with-go/go-how-does-defer-statement-work-1a9492689b6e
作者:Vincent Blanchon[10]譯者:dust347[11]校對(duì):@unknwon[12]
本文由 GCTT[13] 原創(chuàng)編譯,Go 中文網(wǎng)[14] 榮譽(yù)推出
參考資料
defer 語(yǔ)句: https://golang.org/ref/spec#Defer_statements
Golang 規(guī)范: https://golang.org/ref/spec#Defer_statements
[3]匯編: https://golang.org/doc/asm
[4]命名返回參數(shù): https://golang.org/ref/spec#Function_types
[5]匿名函數(shù)( function literal ): https://golang.org/ref/spec#Function_literals
[6]命名返回參數(shù): https://golang.org/ref/spec#Function_types
[7]defer, panic 和 recover: https://blog.golang.org/defer-panic-and-recover
[8]Go 1.8: https://golang.org/doc/go1.8#defer
[9]這個(gè)提升分配方式的 CL : https://go-review.googlesource.com/c/go/+/29656/
[10]Vincent Blanchon: https://medium.com/@blanchon.vincent
[11]dust347: https://github.com/dust347
[12]@unknwon: https://github.com/unknwon
[13]GCTT: https://github.com/studygolang/GCTT
[14]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語(yǔ)言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛(ài)好者值得關(guān)注
