前言
我們知道Golang處理異常是用error返回的方式,然后調用方根據error的值走不同的處理邏輯。但是,如果程序觸發其他的嚴重異常,比如說數組越界,程序就要直接崩潰。Golang有沒有一種異常捕獲和恢復機制呢?這個就是本文要講的panic和recover。其中recover要配合defer使用才能發揮出效果。
Defer
Defer語句將一個函數放入一個列表(用棧表示其實更准確)中,該列表的函數在環繞defer的函數返回時會被執行。defer通常用於簡化函數的各種各樣清理動作,例如關閉文件,解鎖等等的釋放資源的動作。例如下面的這個函數打開兩個文件,從一個文件拷貝內容到另外的一個文件:
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } dst, err := os.Create(dstName) if err != nil { return } written, err = io.Copy(dst, src) dst.Close() src.Close() return }
這段代碼可以工作,但是有一個bug。如果調用os.Create失敗,函數將會直接返回,並沒有關閉srcName文件。修復的方法很簡單,可以把src.Close的調用放在第二個return語句前面。但是當我們程序的分支比較多的時候,也就是說當該函數還有幾個其他的return語句時,就需要在每個分支return前都要加上close動作。這樣使得資源的清理非常繁瑣而且容易遺漏。所以Golang引入了defer語句:
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() return io.Copy(dst, src) }
在每個資源申請成功的后面都加上defer自動清理,不管該函數都多少個return,資源都會被正確的釋放,例如上述例子的文件一定會被關閉。
關閉defer語句,有三條簡單的規則:
1.defer的函數在壓棧的時候也會保存參數的值,並非在執行時取值。
func a() { i := 0 defer fmt.Println(i) i++ return }
例如該示例中,變量i會在defer時就被保存起來,所以defer函數執行時i的值是0.即便后面i的值變為了1,也不會影響之前的拷貝。
2.defer函數調用的順序是后進先出。
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
函數輸出3210
3.defer函數可以讀取和重新賦值函數的命名返回參數。
func c() (i int) { defer func() { i++ }() return 1 }
這個例子中,defer函數中在函數返回時對命名返回值i進行了加1操作,因此函數返回值是2.可能你會有疑問,規則1不是說會在defer時保存i的值嗎?保存的i是0,那加1操作之后也是1啊。這里就是閉包的魅力,i的值會被立馬保存,但是保存的是i的引用,也可以理解為指針。當實際執行加1操作時,i的值其實被return置為了1,defer執行了加1操作i的值也就變成了2.
Panic
Panic是內建的停止控制流的函數。相當於其他編程語言的拋異常操作。當函數F調用了panic,F的執行會被停止,在F中panic前面定義的defer操作都會被執行,然后F函數返回。對於調用者來說,調用F的行為就像調用panic(如果F函數內部沒有把panic recover掉)。如果都沒有捕獲該panic,相當於一層層panic,程序將會crash。panic可以直接調用,也可以是程序運行時錯誤導致,例如數組越界。
Recover
Recover是一個從panic恢復的內建函數。Recover只有在defer的函數里面才能發揮真正的作用。如果是正常的情況(沒有發生panic),調用recover將會返回nil並且沒有任何影響。如果當前的goroutine panic了,recover的調用將會捕獲到panic的值,並且恢復正常執行。
例如下面這個例子:
package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
函數g接受參數i,如果i大於3時觸發panic,否則對i進行加1操作。函數f的defer函數里面調用了recover並且打印recover的值(非nil的話)。
程序將會輸出:
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.
Panic和recover可以接受任何類型的值,因為定義為interface{}:
func panic(v interface{})
func recover() interface{}
所以工作模式相當於:
panic(value)->recover()->value
傳遞給panic的value最終由recover捕獲。
另外defer可以配合鎖的使用來確保鎖的釋放,例如:
mu.Lock()
Defer mu.Unlock()
需要注意的是這樣會延長鎖的釋放時間(需要等到函數return)。
容易踩坑的一些例子
通過上面的說明,我們已經對defer,panic和recover有了比較清晰的認識,下面通過一些實戰中容易踩坑的例子來加深下印象。
在循環里面使用defer
不要在循環里面使用defer,除非你真的確定defer的工作流程,例如:
只有當函數返回時defer的函數才會被執行,如果在for循環里面defer定義的函數會不斷的壓棧,可能會爆棧而導致程序異常。
解決方法1:將defer移動到循環之外
解決方法2:構造一層新的函數包裹defer
defer方法
沒有指針的情況:
type Car struct { model string } func (c Car) PrintModel() { fmt.Println(c.model) } func main() { c := Car{model: "DeLorean DMC-12"} defer c.PrintModel() c.model = "Chevrolet Impala" }
程序輸出DeLorean DMC-12。根據我們前面講的內容,defer的時候會把函數和參考拷貝一份保存起來,所以c.model的值后面改變也不會影響defer的運行。
有指針的情況:
Car PrintModel()方法定義改為:
func (c *Car) PrintModel() {
fmt.Println(c.model)
}
程序將會輸出Chevrolet Impala。這些defer雖然將函數和參數保存了起來,但是由於參數的值本身是針對,隨意后面的改動會影響到defer函數的行為。
同理的例子還有:
for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() }
程序將會輸出:
3 3 3
因為閉包引用匿名函數外面的變量相當於是指針引用,得到的是變量的地址,實際到defer真正執行時,指針指向的內容已經發生的變化:
解決的方法:
for i := 0; i < 3; i++ { defer func(i int) { fmt.Println(i) }(i) }
或者:
for i := 0; i < 3; i++ { defer fmt.Println(i) }
程序輸出:
2 1 0
這里就不會用到閉包的上下文引用特性,是正經的函數參數拷貝傳遞,所以不會有問題。
defer中修改函數error返回值
package main import ( "errors" "fmt" ) func main() { { err := release() fmt.Println(err) } { err := correctRelease() fmt.Println(err) } } func release() error { defer func() error { return errors.New("error") }() return nil } func correctRelease() (err error) { defer func() { err = errors.New("error") }() return nil }
release函數中error的值並不會被defer的return返回,因為匿名返回值在defer執行前就已經聲明好並復制為nil。correctRelease函數能夠修改返回值是因為閉包的特性,defer中的err是實際的返回值err地址引用,指向的是同一個變量。defer修改程序返回值error一般用在和recover搭配中,上述的情況屬於濫用defer的一種情況,其實error函數值可以直接在程序的return中修改,不用defer。
總結
文章介紹了defer、panic和recover的原理和用法,並且在最后給出了一些在實際應用的實踐建議,不要濫用defer,注意defer搭配閉包時的一些特性。
參考
https://blog.golang.org/defer-panic-and-recover
https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
https://blog.learngoprogramming.com/golang-defer-simplified-77d3b2b817ff
https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1