本文將會(huì)講解defer, recover,panic相關(guān)的知識(shí)。主要內(nèi)容包括:
- defer的原理
- panic與recover的原理及注意事項(xiàng)
其中重點(diǎn)在defer的原理。這部分包含了defer的定義、規(guī)則、實(shí)現(xiàn)原理、內(nèi)部函數(shù)順序四部分。
希望看完本文,你能對defer、recover、panic有個(gè)全面的認(rèn)識(shí)~
一、defer的原理
定義
1、defer語句用于延遲函數(shù)的調(diào)用,每次defer都會(huì)把一個(gè)函數(shù)壓入棧中,主函數(shù)(創(chuàng)建defer的函數(shù))返回前再把延遲的函數(shù)取出并執(zhí)行。defer最常見的場景是完成一些收尾的工作,比如文件句柄的關(guān)閉等。還有就是執(zhí)行 recover, 實(shí)現(xiàn)類似其他語言中的try catch finally。
2、延遲函數(shù)可能有輸入?yún)?shù),這些參數(shù)可能來源于定義defer的函數(shù),延遲函數(shù)也可能引用主函數(shù)用于返回的變量,也就是說延遲函數(shù)可能會(huì)影響主函數(shù)的一些行為。
規(guī)則
規(guī)則一、其實(shí)使用defer時(shí),用一個(gè)簡單的轉(zhuǎn)換規(guī)則改寫一下,就不會(huì)迷糊了。改寫規(guī)則是將return語句拆成兩句寫,return xxx會(huì)被改寫成:
返回值 = xxx
調(diào)用defer的函數(shù)
空的return
規(guī)則一的幾個(gè)案例
下面通過一些案例進(jìn)行總結(jié):
題目一
func deferFuncParameter() {
var aInt = 1
defer fmt.Println(aInt)
aInt = 2
return
}
延遲函數(shù)fmt.Println(aInt) 在defer語句出現(xiàn)時(shí)就已經(jīng)確定了,所以無論后面如何修改aInt的值都不會(huì)影響延遲函數(shù)。
上述程序轉(zhuǎn)換之后是這樣的:
func deferFuncParameter() {
var aInt = 1
anonymous = aInt // anonymous為匿名的變量
aInt = 2
fmt.Println(anonymous)
return
}
即使是結(jié)構(gòu)體,也是傳值,也不會(huì)影響。比如
type Test struct {
value int
}
func (t Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
這段代碼輸出的也是0.
如果是結(jié)構(gòu)體指針,則會(huì)影響輸出。
type Test struct {
value int
}
func (t *Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
這個(gè)輸出的就是1, 因?yàn)閭鬟f的指針。
題目二
func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(&aArray)
aArray[0] = 10
return
}
func main() {
deferFuncParameter()
}
函數(shù)deferFuncParameter定義了一個(gè)數(shù)組,通過defer調(diào)用printArray, 最后修改數(shù)組的第一個(gè)元素。printArray 函數(shù)接收數(shù)組的指針,即數(shù)組的地址,由于延遲函數(shù)執(zhí)行時(shí)機(jī)在return語句之前,所以對數(shù)組的最終修改值被打印出來。
題目三
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
函數(shù)的return語句并不是原子的,實(shí)際執(zhí)行分為設(shè)置返回值->ret。defer語句實(shí)際執(zhí)行在主函數(shù)返回(ret)前,即擁有defer的函數(shù)返回過程是 : 設(shè)置返回值->執(zhí)行defer->ret。所以return語句先把result設(shè)置為i的值,即1,defer語句中又把result遞增1,所以最終返回的是2.
上述程序可以轉(zhuǎn)換為
func deferFuncReturn() (result int) {
i := 1
result = i
func() {
result++
}()
return
}
總結(jié)上文的例子可以得出如下幾個(gè)結(jié)論:
- 延遲函數(shù)的參數(shù)在defer語句出現(xiàn)時(shí)就已經(jīng)確定下來了
如果是字面量,則肯定不受影響(如題目一); 如果是指針類型,規(guī)則仍然適用,只不過延遲函數(shù)的參數(shù)是一個(gè)地址值,這種情況下defer后面的語句對變量的修改可能會(huì)影響延遲函數(shù)(如題目二)。
- 延遲函數(shù)可能操作主函數(shù)的具名返回值
關(guān)鍵字return不是一個(gè)原子操作,實(shí)際上return只代理匯編指令ret,即將跳轉(zhuǎn)程序執(zhí)行。比如語句return i,實(shí)際上分兩步進(jìn)行,即將i值存入棧中作為返回值,然后執(zhí)行跳轉(zhuǎn),而defer的執(zhí)行時(shí)機(jī)正是跳轉(zhuǎn)前,所以說defer執(zhí)行時(shí)還是有機(jī)會(huì)操作返回值的。
規(guī)則二、從主函數(shù)返回值的角度看,有如下的幾條規(guī)則:
1、主函數(shù)擁有匿名返回值,返回字面值
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
一個(gè)主函數(shù)擁有一個(gè)匿名的返回值,返回時(shí)使用字面值,比如”1“, ”hello“這樣的值,這種情況下defer是無法操作返回值的。
2、主函數(shù)擁有匿名返回值,返回變量
一個(gè)主函數(shù)擁有一個(gè)匿名的返回值,返回使用本地或全局變量,這種情況下defer語句可以引用到返回值,但不會(huì)改變返回值。
func foo() int {
var i int
defer func() {
i++
}()
return i
}
上面的函數(shù),返回一個(gè)局部變量,同時(shí)defer函數(shù)也會(huì)操作這個(gè)局部變量。對于匿名返回值來說,可以假定仍然有一個(gè)變量存儲(chǔ)返回值,假定返回值變量為"anony",上面的返回語句可以拆分成以下過程:
anony = i
i++
return
由于i是整型,會(huì)將值拷貝給anony,所以defer語句修改i值,對函數(shù)返回值不會(huì)造成影響。
3、主函數(shù)擁有具名返回值
主函數(shù)聲明語句中帶有名字的返回值,會(huì)被初始化一個(gè)局部變量,函數(shù)內(nèi)部可以像使用局部變量一樣使用該返回值。如果defer語句操作該返回值,可能會(huì)改變返回結(jié)果。
func foo() (ret int) {
defer func() {
ret++
}()
return 0}
上面的函數(shù)拆解之后是這樣的:
ret = 0
ret++
return
defer實(shí)現(xiàn)原理
數(shù)據(jù)結(jié)構(gòu)
每個(gè)goroutine數(shù)據(jù)結(jié)構(gòu)中實(shí)際上也有一個(gè)defer指針,該指針指向一個(gè)defer的單鏈表,每次聲明一個(gè)defer時(shí)就將defer插入到單鏈表表頭,每次執(zhí)行defer時(shí)就從單鏈表表頭取出一個(gè)defer執(zhí)行。
defer的創(chuàng)建和執(zhí)行
源碼包src/runtime/panic.go定義了兩個(gè)方法分別用于創(chuàng)建defer和執(zhí)行defer。
- deferproc(): 在聲明defer處調(diào)用,其將defer函數(shù)存入goroutine的鏈表中;
- deferreturn():在return指令,準(zhǔn)確的講是在ret指令前調(diào)用,其將defer從goroutine鏈表中取出并執(zhí)行。
可以簡單這么理解,在編譯在階段,聲明defer處插入了函數(shù)deferproc(),在函數(shù)return前插入了函數(shù)deferreturn()。
defer內(nèi)部函數(shù)順序
func TestDefer(t *testing.T) {
fmt.Println("a")
defer fmt.Println("b")
defer c()
defer d()
fmt.Println("f")
}
func c() {
fmt.Println("c")
}
func d() func(){
fmt.Println("d")
return func() {
fmt.Println("e")
}
}
輸出為 a f d c b
結(jié)論:defer函數(shù)在函數(shù)執(zhí)行結(jié)束后執(zhí)行,若有多個(gè)defer函數(shù),則執(zhí)行順序?yàn)楹筮M(jìn)先出。 主函數(shù)中,defer d() 并沒有執(zhí)行d()返回的閉包,所以結(jié)果里面并沒有返回e.
func TestDefer(t *testing.T) {
fmt.Println("a")
defer fmt.Println("b")
defer c()
defer d()()
fmt.Println("f")
}
func c() {
fmt.Println("c")
}
func d() func(){
fmt.Println("d")
return func() {
fmt.Println("e")
}
}
這段代碼只是在調(diào)用d方法時(shí)加了個(gè)括號(hào),那么d方法返回的方法就會(huì)立即執(zhí)行
返回結(jié)果為 a d f e c b 。 為什么不是 a f d e c b 呢? 這是因?yàn)樵赿efer d()() 編譯時(shí),首先定義了函數(shù)d(), 此時(shí)就輸出了d. 然后返回包含e的閉包函數(shù)。
即被defer標(biāo)記的d函數(shù)中的程序“立即執(zhí)行”,而d函數(shù)返回的函數(shù)則在測試方法結(jié)束后 按照“后進(jìn)先出”的順序執(zhí)行。
再看一個(gè) 來自effective go的例子:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
結(jié)果會(huì)打印
entering: b
in b
entering: a
in a
leaving: a
leaving: b
沒執(zhí)行以前我以為是如下的結(jié)果:
in b
in a
entring: a
leaving: a
entring: b
leaving: b
為什么不對呢?
因?yàn)樵诰幾g時(shí),b() 中的 defer un(trace("b")) ,是對un()函數(shù)的延遲,但是此時(shí)會(huì)執(zhí)行trace("b")。
這個(gè)例子說明,defer標(biāo)記的函數(shù)只是最外層的函數(shù),如果defer標(biāo)記函數(shù)的參數(shù)也是個(gè)函數(shù),則作為參數(shù)的函數(shù)在編譯時(shí)就會(huì)被執(zhí)行了,不必等到defer標(biāo)記函數(shù)執(zhí)行時(shí)才執(zhí)行。
二、panic與recover的原理及注意事項(xiàng)
- panic內(nèi)置函數(shù)停止當(dāng)前goroutine的正常執(zhí)行,當(dāng)函數(shù)F調(diào)用panic時(shí),函數(shù)F的正常執(zhí)行被立即停止,然后運(yùn)行所有在F函數(shù)中的defer函數(shù),然后F返回到調(diào)用他的函數(shù)對于調(diào)用者G,F(xiàn)函數(shù)的行為就像panic一樣,終止G的執(zhí)行并運(yùn)行G中所defer函數(shù),此過程會(huì)一直繼續(xù)執(zhí)行到goroutine所有的函數(shù)。panic可以通過內(nèi)置的recover來捕獲。
- recover內(nèi)置函數(shù)用來管理含有panic行為的goroutine,recover運(yùn)行在defer函數(shù)中,獲取panic拋出的錯(cuò)誤值,并將程序恢復(fù)成正常執(zhí)行的狀態(tài)。如果在defer函數(shù)之外調(diào)用recover,那么recover不會(huì)停止并且捕獲panic錯(cuò)誤如果goroutine中沒有panic或者捕獲的panic的值為nil,recover的返回值也是nil。由此可見,recover的返回值表示當(dāng)前goroutine是否有panic行為
幾個(gè)注意的問題
1、defer 表達(dá)式的函數(shù)如果定義在 panic 后面,該函數(shù)在 panic 后就無法被執(zhí)行到
func main() {
panic("a")
defer func() {
fmt.Println("b")
}()
}
結(jié)果 b沒有打印出來
而在defer后panic
func main() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
結(jié)果b被正常打印。
2、F中出現(xiàn)panic時(shí),F(xiàn)函數(shù)會(huì)立刻終止,不會(huì)執(zhí)行F函數(shù)內(nèi)panic后面的內(nèi)容,但不會(huì)立刻return,而是調(diào)用F的defer,如果F的defer中有recover捕獲,則F在執(zhí)行完defer后正常返回,調(diào)用函數(shù)F的函數(shù)G繼續(xù)正常執(zhí)行
func G() {
defer func() {
fmt.Println("c")
}()
F()
fmt.Println("繼續(xù)執(zhí)行")
}
func F() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕獲異常:", err)
}
fmt.Println("b")
}()
panic("a")
}
結(jié)果
捕獲異常: a
b
繼續(xù)執(zhí)行
c
3、如果F的defer中無recover捕獲,則將panic拋到G中,G函數(shù)會(huì)立刻終止,不會(huì)執(zhí)行G函數(shù)內(nèi)后面的內(nèi)容,但不會(huì)立刻return,而調(diào)用G的defer...以此類推
func G() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕獲異常:", err)
}
fmt.Println("c")
}()
F()
fmt.Println("繼續(xù)執(zhí)行")
}
func F() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
結(jié)果
b
捕獲異常: a
c
4、如果一直沒有recover,拋出的panic到當(dāng)前goroutine最上層函數(shù)時(shí),程序直接異常終止
func G() {
defer func() {
fmt.Println("c")
}()
F()
fmt.Println("繼續(xù)執(zhí)行")
}
func F() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
結(jié)果
b
c
panic: a
goroutine 1 [running]:
main.F()
/xxxxx/src/xxx.go:61 +0x55
main.G()
/xxxxx/src/xxx.go:53 +0x42
exit status 2
5、recover都是在當(dāng)前的goroutine里進(jìn)行捕獲的,這就是說,對于創(chuàng)建goroutine的外層函數(shù),如果goroutine內(nèi)部發(fā)生panic并且內(nèi)部沒有用recover,外層函數(shù)是無法用recover來捕獲的,這樣會(huì)造成程序崩潰
func G() {
defer func() {
//goroutine外進(jìn)行recover
if err := recover(); err != nil {
fmt.Println("捕獲異常:", err)
}
fmt.Println("c")
}()
//創(chuàng)建goroutine調(diào)用F函數(shù)
go F()
time.Sleep(time.Second)
}
func F() {
defer func() {
fmt.Println("b")
}()
//goroutine內(nèi)部拋出panic
panic("a")
}
結(jié)果:
b
panic: a
goroutine 5 [running]:
main.F()
/xxxxx/src/xxx.go:67 +0x55
created by main.main
/xxxxx/src/xxx.go:58 +0x51
exit status 2
6、recover返回的是interface{}類型而不是go中的 error 類型,如果外層函數(shù)需要調(diào)用err.Error(),會(huì)編譯錯(cuò)誤,也可能會(huì)在執(zhí)行時(shí)panic
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕獲異常:", err.Error())
}
}()
panic("a")
}
編譯錯(cuò)誤,結(jié)果
err.Error undefined (type interface {} is interface with no methods)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕獲異常:", fmt.Errorf("%v", err).Error())
}
}()
panic("a")
}
結(jié)果:
捕獲異常: a
參考文獻(xiàn)
Go defer實(shí)現(xiàn)原理剖析
理解 Go 語言 defer 關(guān)鍵字的原理
defer關(guān)鍵字
golang中的defer函數(shù)的執(zhí)行順序
go defer,panic,recover詳解 go 的異常處理
effective_go中文版
談?wù)?panic 和 recover 的原理,講的比較深入