恐慌和恢復(fù)原理已經(jīng)在前面的文章中介紹過了。 一些恐慌和恢復(fù)用例也在上一篇文章中得到了展示。 本文將詳細(xì)解釋一下恐慌和恢復(fù)原理。函數(shù)調(diào)用的退出階段也將被一并詳細(xì)解釋。
在Go中,一個函數(shù)調(diào)用在其退出完畢之前可能將經(jīng)歷一個退出階段。 在此退出階段,所有在執(zhí)行此函數(shù)調(diào)用期間被推入延遲調(diào)用隊(duì)列的延遲函數(shù)調(diào)用將按照它們的推入順序的逆序被執(zhí)行。 當(dāng)這些延遲函數(shù)調(diào)用都退出完畢之后,此函數(shù)調(diào)用的退出階段也就結(jié)束了,或者說此函數(shù)調(diào)用也退出完畢了,
退出階段有時候也被稱為返回階段。
一個函數(shù)調(diào)用可能通過三種途徑進(jìn)入它的退出階段:
runtime.Goexit
?函數(shù)在此調(diào)用中被調(diào)用并且退出完畢。比如,在下面這段代碼中,
f0
或者f1
的一個調(diào)用將在它正常返回后進(jìn)入它的退出階段;f2
的一個調(diào)用將在“被零除”恐慌產(chǎn)生之后進(jìn)入它的退出階段;f3
的一個調(diào)用將在其中的runtime.Goexit
函數(shù)調(diào)用退出完畢之后進(jìn)入它的退出階段。import (
"fmt"
"runtime"
)
func f0() int {
var x = 1
defer fmt.Println("正常退出:", x)
x++
return x
}
func f1() {
var x = 1
defer fmt.Println("正常退出:", x)
x++
}
func f2() {
var x, y = 1, 0
defer fmt.Println("因恐慌而退出:", x)
x = x / y // 將產(chǎn)生一個恐慌
x++ // 執(zhí)行不到
}
func f3() int {
x := 1
defer fmt.Println("因Goexit調(diào)用而退出:", x)
x++
runtime.Goexit()
return x+x // 執(zhí)行不到
}
順便說一下,一般runtime.Goexit()
函數(shù)不希望在主協(xié)程中調(diào)用。
當(dāng)一個函數(shù)調(diào)用中直接產(chǎn)生了一個恐慌的時候,我們可以認(rèn)為此(尚未被恢復(fù)的)恐慌將和此函數(shù)調(diào)用相關(guān)聯(lián)起來。 類似地,當(dāng)一個函數(shù)調(diào)用直接調(diào)用了runtime.Goexit
函數(shù),則runtime.Goexit
函數(shù)返回完畢之后,我們可以認(rèn)為一個Goexit信號將和此函數(shù)調(diào)用相關(guān)聯(lián)起來。 按照上一節(jié)中的解釋,當(dāng)一個恐慌或者一個Goexit信號和一個函數(shù)調(diào)用相關(guān)聯(lián)之后,此函數(shù)調(diào)用將立即進(jìn)入它的退出階段。
我們已經(jīng)了解到恐慌是可以被恢復(fù)的。 但是,Goexit信號是不能被取消的。
在任何一個給定時刻,一個函數(shù)調(diào)用最多只能和一個未恢復(fù)的恐慌相關(guān)聯(lián)。 如果一個調(diào)用正和一個未恢復(fù)的恐慌相關(guān)聯(lián),則
比如,在下面這個例子中,最終被恢復(fù)的恐慌是恐慌3。它是最后一個和main
函數(shù)調(diào)用相關(guān)聯(lián)的恐慌。
package main
import "fmt"
func main() {
defer func() {
fmt.Println(recover()) // 3
}()
defer panic(3) // 將替換恐慌2
defer panic(2) // 將替換恐慌1
defer panic(1) // 將替換恐慌0
panic(0)
}
因?yàn)镚oexit信號不可被取消,爭論一個函數(shù)調(diào)用是否最多只能和一個Goexit信號相關(guān)聯(lián)是沒有意義和沒有必要的。
在某個時刻,一個協(xié)程中可能共存多個未被恢復(fù)的恐慌,盡管這在實(shí)際編程中并不常見。 每個未被恢復(fù)的恐慌和此協(xié)程的調(diào)用堆棧中的一個尚未退出的函數(shù)調(diào)用相關(guān)聯(lián)。 當(dāng)仍和一個未被恢復(fù)的恐慌相關(guān)聯(lián)的一個內(nèi)層函數(shù)調(diào)用退出完畢之后,此未被恢復(fù)的恐慌將傳播到調(diào)用此內(nèi)層函數(shù)調(diào)用的外層函數(shù)調(diào)用中。 這和在此外層函數(shù)調(diào)用中直接產(chǎn)生一個新的恐慌的效果是一樣的。也就是說,
所以,當(dāng)一個協(xié)程完成完畢后,此協(xié)程中最多只有一個尚未被恢復(fù)的恐慌。 如果一個協(xié)程帶著一個尚未被恢復(fù)的恐慌退出完畢,則這將使整個程序崩潰,此恐慌信息將在程序崩潰的時候被打印出來。
在一個函數(shù)調(diào)用被執(zhí)行的起始時刻,此調(diào)用將沒有任何恐慌和Goexit信號和它相關(guān)聯(lián),這個事實(shí)和此函數(shù)調(diào)用的外層調(diào)用是否已經(jīng)進(jìn)入退出階段無關(guān)。 當(dāng)然,在此函數(shù)調(diào)用的執(zhí)行過程中,恐慌可能產(chǎn)生,runtime.Goexit
函數(shù)也可能被調(diào)用,因此恐慌和Goexit信號以后可能和此調(diào)用相關(guān)聯(lián)起來。
下面這個例子程序在運(yùn)行時將崩潰,因?yàn)樾麻_辟的協(xié)程在退出完畢時仍帶有一個未被恢復(fù)的恐慌。
package main
func main() {
// 新開辟一個協(xié)程。
go func() {
// 一個匿名函數(shù)調(diào)用。
// 當(dāng)它退出完畢時,恐慌2將傳播到此新協(xié)程的入口
// 調(diào)用中,并且替換掉恐慌0??只?永不會被恢復(fù)。
defer func() {
// 上一個例子中已經(jīng)解釋過了:恐慌2將替換恐慌1.
defer panic(2)
// 當(dāng)此匿名函數(shù)調(diào)用退出完畢后,恐慌1將傳播到剛
// 提到的外層匿名函數(shù)調(diào)用中并與之關(guān)聯(lián)起來。
func () {
panic(1)
// 在恐慌1產(chǎn)生后,此新開辟的協(xié)程中將共存
// 兩個未被恢復(fù)的恐慌。其中一個(恐慌0)
// 和此協(xié)程的入口函數(shù)調(diào)用相關(guān)聯(lián);另一個
// (恐慌1)和當(dāng)前這個匿名調(diào)用相關(guān)聯(lián)。
}()
}()
panic(0)
}()
select{}
}
此程序的輸出(當(dāng)使用標(biāo)準(zhǔn)編譯器1.19版本編譯):
panic: 0
panic: 1
panic: 2
...
此輸出的格式并非很完美,它容易讓一些程序員誤認(rèn)為恐慌0是最終未被恢復(fù)的恐慌。而事實(shí)上,恐慌2才是最終未被恢復(fù)的恐慌。
類似地,當(dāng)一個和Goexit信號相關(guān)聯(lián)的內(nèi)層函數(shù)調(diào)用退出完畢后,此Goexit信號也將傳播到外層函數(shù)調(diào)用中,并和外層函數(shù)調(diào)用相關(guān)聯(lián)起來。 如果外層函數(shù)調(diào)用尚未進(jìn)入退出階段,則其將立即進(jìn)入。
當(dāng)一個Goexit信號和一個函數(shù)調(diào)用相關(guān)聯(lián)起來的時候,如果此函數(shù)調(diào)用正在和一個未被恢復(fù)的恐慌相關(guān)聯(lián)著,則此恐慌將被恢復(fù)。 比如下面這個程序?qū)⒄M顺霾⒋蛴〕?code><nil>,因?yàn)榭只?code>bye被Goexit信號恢復(fù)了。
package main
import (
"fmt"
"runtime"
)
func f() {
defer func() {
fmt.Println(recover())
}()
// 此調(diào)用產(chǎn)生的Goexit信號恢復(fù)之前產(chǎn)生的恐慌。
defer runtime.Goexit()
panic("bye")
}
func main() {
go f()
for runtime.NumGoroutine() > 1 {
runtime.Gosched()
}
}
內(nèi)置recover
函數(shù)必須在合適的位置調(diào)用才能發(fā)揮作用;否則,它的調(diào)用相當(dāng)于空操作。 比如,在下面這個程序中,沒有一個recover
函數(shù)調(diào)用恢復(fù)了恐慌bye
。
package main
func main() {
defer func() {
defer func() {
recover() // 空操作
}()
}()
defer func() {
func() {
recover() // 空操作
}()
}()
func() {
defer func() {
recover() // 空操作
}()
}()
func() {
defer recover() // 空操作
}()
func() {
recover() // 空操作
}()
recover() // 空操作
defer recover() // 空操作
panic("bye")
}
我們已經(jīng)知道下面這個recover
調(diào)用是有作用的。
package main
func main() {
defer func() {
recover() // 將恢復(fù)恐慌"byte"
}()
panic("bye")
}
那么為什么本節(jié)中的第一個例子中的所有recover
調(diào)用都不起作用呢? 讓我們先看看當(dāng)前版本的Go白皮書是怎么說的:
在下面的情況下,recover
函數(shù)調(diào)用的返回值為nil
:
panic
函數(shù)調(diào)用的實(shí)參為nil;recover
函數(shù)并未直接在一個延遲函數(shù)調(diào)用中調(diào)用。
上一篇文章中提供了一個第一種情況的例子。
本節(jié)中的第一個例子中的大多recover
調(diào)用要么符合Go白皮書中描述的第二種情況,要么符合第三種情況,除了第一個recover
調(diào)用。 是的,當(dāng)前版本的白皮書中的描述并不準(zhǔn)確。第三種情況應(yīng)該更精確地描述為:
recover
?函數(shù)并未直接被一個延遲函數(shù)調(diào)用所直接調(diào)用, 或者它直接被一個延遲函數(shù)調(diào)用所直接調(diào)用但是此延遲調(diào)用沒有被和期望被恢復(fù)的恐慌相關(guān)聯(lián)的函數(shù)調(diào)用所直接調(diào)用。在本節(jié)中的第一個例子中,期望被恢復(fù)的恐慌和main
函數(shù)調(diào)用相關(guān)聯(lián)。 第一個recover
調(diào)用確實(shí)被一個延遲函數(shù)調(diào)用所直接調(diào)用,但是此延遲函數(shù)調(diào)用并沒有被main
函數(shù)直接調(diào)用。 這就是為什么此recover
調(diào)用是一個空操作的原因。
事實(shí)上,當(dāng)前版本的白皮書也沒有解釋清楚為什么下面這個例子中的第二個recover
調(diào)用(按照代碼行順序)沒有起作用。 此調(diào)用本期待用來恢復(fù)恐慌1。
// 此程序?qū)е幢换謴?fù)的恐慌1而崩潰退出。
package main
func demo() {
defer func() {
defer func() {
recover() // 此調(diào)用將恢復(fù)恐慌2
}()
defer recover() // 空操作
panic(2)
}()
panic(1)
}
func main() {
demo()
}
當(dāng)前版本的白皮書沒提到的一點(diǎn)是:每個recover
調(diào)用都試圖恢復(fù)當(dāng)前協(xié)程中最新產(chǎn)生的且尚未恢復(fù)的恐慌。 當(dāng)然,如果這個假設(shè)中的最新產(chǎn)生的且尚未恢復(fù)的恐慌不存在,則此recover
調(diào)用是一個空操作。
Go運(yùn)行時認(rèn)為上例中的第二個recover
調(diào)用試圖恢復(fù)最新產(chǎn)生的尚未恢復(fù)的恐慌,即恐慌2。 而此時和恐慌2相關(guān)聯(lián)的函數(shù)調(diào)用為此第二個recover
調(diào)用的直接調(diào)用者,即外層的延遲函數(shù)調(diào)用。 此第二個recover
調(diào)用并沒有被外層的延遲函數(shù)調(diào)用所直接調(diào)用的某個延遲函數(shù)調(diào)用所調(diào)用; 相反,它直接被外層的延遲函數(shù)調(diào)用所調(diào)用。這就是為什么此第二個recover
調(diào)用是一個空操作的原因。
好了,到此我們可以對哪些recover
調(diào)用會起作用做一個簡短的描述:
一個recover
調(diào)用只有在它的直接外層調(diào)用(即recover
調(diào)用的父調(diào)用)是一個延遲調(diào)用,并且此延遲調(diào)用(即父調(diào)用)的直接外層調(diào)用(即recover
調(diào)用的爺調(diào)用)和當(dāng)前協(xié)程中最新產(chǎn)生并且尚未恢復(fù)的恐慌相關(guān)聯(lián)時才起作用。 一個有效的recover
調(diào)用將最新產(chǎn)生并且尚未恢復(fù)的恐慌和與此恐慌相關(guān)聯(lián)的函數(shù)調(diào)用(即爺調(diào)用)剝離開來,并且返回當(dāng)初傳遞給產(chǎn)生此恐慌的panic
函數(shù)調(diào)用的參數(shù)。
更多建議: