Go基礎(chǔ)教程系列之defer、panic和recover詳解
defer關(guān)鍵字
defer關(guān)鍵字可以讓函數(shù)或語句延遲到函數(shù)語句塊的最結(jié)尾時(shí),即即將退出函數(shù)時(shí)執(zhí)行,即便函數(shù)中途報(bào)錯(cuò)結(jié)束、即便已經(jīng)panic()、即便函數(shù)已經(jīng)return了,也都會執(zhí)行defer所推遲的對象。
其實(shí)defer的本質(zhì)是,當(dāng)在某個(gè)函數(shù)中使用了defer關(guān)鍵字,則創(chuàng)建一個(gè)獨(dú)立的defer棧幀,并將該defer語句壓入棧中,同時(shí)將其使用的相關(guān)變量也拷貝到該棧幀中(顯然是按值拷貝的)。因?yàn)闂J荓IFO方式,所以先壓棧的后執(zhí)行。因?yàn)槭仟?dú)立的棧幀,所以即使調(diào)用者函數(shù)已經(jīng)返回或報(bào)錯(cuò),也一樣能在它們之后進(jìn)入defer棧幀去執(zhí)行。
例如:
func main() {
a()
}
func a() {
println("in a")
defer b() // 將b()壓入defer棧中
println("leaving a")
//到了這里才會執(zhí)行b()
}
func b() {
println("in b")
println("leaving b")
}
上面將輸出:
in a leaving a in b leaving b
即便是函數(shù)已經(jīng)報(bào)錯(cuò),或函數(shù)已經(jīng)return返回,defer的對象也會在函數(shù)退出前的最后一刻執(zhí)行。
func a() TYPE{
...CODE...
defer b()
...CODE...
// 函數(shù)執(zhí)行出了錯(cuò)誤
return args
// 函數(shù)b()都會在這里執(zhí)行
}
但注意,由于Go的作用域采用的是詞法作用域,defer的定義位置決定了它推遲對象能看見的變量值,而不是推遲對象被調(diào)用時(shí)所能看見的值。
例如:
package main
var x = 10
func main() {
a()
}
func a() {
println("start a:",x) // 輸出10
x = 20
defer b(x) // 壓棧,并按值拷貝20到棧中
x = 30
println("leaving a:",x) // 輸出30
// 調(diào)用defer延遲的對象b(),輸出20
}
func b(x int) {
println("start b:",x)
}
比較下面的defer:
package main
var x = 10
func main() {
a()
}
func a() int {
println("start a:", x) // 輸出10
x = 20
defer func() { // 壓棧,但并未傳值,所以內(nèi)部引用x
println("in defer:", x) // 輸出30
}()
x = 30
println("leaving a:", x) // 輸出30
return x
}
上面defer推遲的匿名函數(shù)輸出的值是30,它看見的不應(yīng)該是20嗎?先再改成下面的:
package main
var x = 10
func main() {
a()
}
func a() int {
println("start a:", x) // 輸出10
x = 20
defer func(x int) {
println("in defer:", x) // 輸出20
}(x)
x = 30
println("leaving a:", x) // 輸出30
return x
}
這個(gè)defer推遲的對象中看見的卻是20,這和第一種defer b(x)是相同的。
原因在于defer推遲的如果是函數(shù),它直接就在它的定義位置處評估好參數(shù)、變量。該拷貝傳值的拷貝傳值,該指針相見的指針相見。所以,對于第(1)和第(3)種情況,在defer的定義位置處,就將x=20拷貝給了推遲的函數(shù)參數(shù),所以函數(shù)內(nèi)部操作的一直是x的副本。而第二種情況則是直接指向它所看見的x=20那個(gè)變量,則個(gè)變量是全局變量,當(dāng)執(zhí)行x=30的時(shí)候會將其值修改,到執(zhí)行defer推遲的對象時(shí),它指向的x的值已經(jīng)是修改過的。
再看下面這個(gè)例子,將defer放進(jìn)一個(gè)語句塊中,并在這個(gè)語句塊中新聲明一個(gè)同名變量x:
func a() int {
println("start a:", x) // 輸出10
x = 20
{
x := 40
defer func() {
println("in defer:", x) // 輸出40
}()
}
x = 30
println("leaving a:", x) // 輸出30
return x
}
上面的defer定義在語句塊中,它能看見的x是語句塊中x=40,它的x指向的是語句塊中的x。另一方面,當(dāng)語句塊結(jié)束時(shí),x=40的x會消失,但由于defer的函數(shù)中仍有x指向40這個(gè)值,所以40這個(gè)值仍被defer的函數(shù)引用著,它直到defer執(zhí)行完之后才會被GC回收。所以defer的函數(shù)在執(zhí)行的時(shí)候,仍然會輸出40。
如果語句塊內(nèi)有多個(gè)defer,則defer的對象以LIFO(last in first out)的方式執(zhí)行,也就是說,先定義的defer后執(zhí)行。
func main() {
println("start...")
defer println("1")
defer println("2")
defer println("3")
defer println("4")
println("end...")
}
將輸出:
start... end... 4 3 2 1
defer有什么用呢?一般用來做善后操作,例如清理垃圾、釋放資源,無論是否報(bào)錯(cuò)都執(zhí)行defer對象。另一方面,defer可以讓這些善后操作的語句和開始語句放在一起,無論在可讀性上還是安全性上都很有改善,畢竟寫完開始語句就可以直接寫defer語句,永遠(yuǎn)也不會忘記關(guān)閉、善后等操作。
例如,打開文件,關(guān)閉文件的操作寫在一起:
open() defer file.Close() ... 操作文件 ...
以下是defer的一些常用場景:
- 打開關(guān)閉文件
- 鎖定、釋放鎖
- 建立連接、釋放連接
- 作為結(jié)尾輸出結(jié)尾信息
- 清理垃圾(如臨時(shí)文件)
panic()和recover()
panic()用于產(chǎn)生錯(cuò)誤信息并終止當(dāng)前的goroutine,一般將其看作是退出panic()所在函數(shù)以及退出調(diào)用panic()所在函數(shù)的函數(shù)。例如,G()中調(diào)用F(),F(xiàn)()中調(diào)用panic(),則F()退出,G()也退出。
注意,defer關(guān)鍵字推遲的對象是函數(shù)最后調(diào)用的,即使出現(xiàn)了panic也會調(diào)用defer推遲的對象。
例如,下面的代碼中,main()中輸出一個(gè)start main之后調(diào)用a(),它會輸出start a,然后就panic了,panic()會輸出panic: panic in a,然后報(bào)錯(cuò),終止程序。
func main() {
println("start main")
a()
println("end main")
}
func a() {
println("start a")
panic("panic in a")
println("end a")
}
執(zhí)行結(jié)果如下:
start main
start a
panic: panic in a
goroutine 1 [running]:
main.a()
E:/learning/err.go:14 +0x63
main.main()
E:/learning/err.go:8 +0x4c
exit status 2
注意上面的end a和end main都沒有被輸出。
可以使用recover()去捕獲panic()并恢復(fù)執(zhí)行。recover()用于捕捉panic()錯(cuò)誤,并返回這個(gè)錯(cuò)誤信息。但注意,即使recover()捕獲到了panic(),但調(diào)用含有panic()函數(shù)的函數(shù)(即上面的G()函數(shù))也會退出,所以如果recover()定義在G()中,則G()中調(diào)用F()函數(shù)之后的代碼都不會執(zhí)行(見下面的通用格式)。
以下是比較通用的panic()和recover()的格式:
func main() {
G()
// 下面的代碼會執(zhí)行
...CODE IN MAIN...
}
func G(){
defer func (){
if str := recover(); str != nil {
fmt.Println(str)
}
}()
...CODE IN G()...
// F()的調(diào)用必須在defer關(guān)鍵字之后
F()
// 該函數(shù)內(nèi)下面的代碼不會執(zhí)行
...CODE IN G()...
}
func F() {
...CODE1...
panic("error found")
// 下面的代碼不會執(zhí)行
...CODE IN F()...
}
可以使用recover()去捕獲panic()并恢復(fù)執(zhí)行。但以下代碼是錯(cuò)誤的:
func main() {
println("start main")
a()
println("end main")
}
func a() {
println("start a")
panic("panic in a")
// 直接放在panic后是錯(cuò)誤的
panic_str := recover()
println(panic_str)
println("end a")
}
之所以錯(cuò)誤,是因?yàn)閜anic()一出現(xiàn)就直接退出函數(shù)a()和main()了。要想recover()真正捕獲panic(),需要將recover()放在defer的推遲對象中,且defer的定義必須在panic()發(fā)生之前。
例如,下面是通用格式的示例:
package main
import "fmt"
func main() {
println("start main")
b()
println("end main")
}
func a() {
println("start a")
panic("panic in a")
println("end a")
}
func b() {
println("start b")
defer func() {
if str := recover(); str != nil {
fmt.Println(str)
}
}()
a()
println("end b")
}
以下是輸出結(jié)果:
start main start b start a panic in a end main
注意上面的end b、end a都沒有被輸出,但是end main輸出了。
panic()是內(nèi)置的函數(shù)(在包builtin中),在log包中也有一個(gè)Panic()函數(shù),它調(diào)用Print()輸出信息后,再調(diào)用panic()。go doc log Panic一看便知:
$ go doc log Panic
func Panic(v ...interface{})
Panic is equivalent to Print() followed by a call to panic().
更多關(guān)于 Go基礎(chǔ)教程系列之defer、panic和recover詳解 請查看下面的相關(guān)鏈接
相關(guān)文章
golang優(yōu)先級隊(duì)列的實(shí)現(xiàn)全過程
Golang使用cobra實(shí)現(xiàn)命令行程序的示例代碼
Go?web中cookie值安全securecookie庫使用原理
golang?select?機(jī)制和超時(shí)問題

