Go 中的Defer,Panic 和 Recover 控制流
- 原文標題:Defer, Panic, and Recover
- 原文作者:Andrew Gerrand
- 原文時間:2010-08-04
Go 語言有通用的控制流機制:if
, for
, switch
, goto
。還有 go
語句將程式碼執行在不同的 goroutine 中。這裡我要討論下幾個不是那麼常用的控制流:defer
, panic
和 recover
。
defer
一個 defer 語句會將一個函式呼叫加到特定的列表中。在某個函式返回的時候,這個列表中儲存的函式將被執行。Defer 通常用於簡化那些有很多清理動作的函式。
例如,下面這個函式開啟了兩個檔案,然後將其中一個檔案的內容複製到另一箇中:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != null {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
複製程式碼
這樣寫能正常執行,但是有個 bug。如果函式 os.create
呼叫失敗,整個函式將直接返回,而原始檔卻沒有關閉。修復這個 bug 非常簡單,只要在第二個 return 語句前加上 src.Close
即可。但如果這個函式變得非常複雜,那這個 bug 就很難被發現和解決。通過引入 defer 語言,我們總是能保證開啟的檔案能關閉:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != null {
return
}
defer src.Close()
des, err := os.Create(dstName)
if err != nil {
return
}
def dst.Close()
return io.Copy(dst, src)
}
複製程式碼
Defer 語句能讓我們在開啟一個檔案之後馬上考慮關閉的情況,而且是有保證的,函式中無論有多少個 return 語句,開啟的檔案總能關閉。
Defer 語句的行為是非常簡單和可預測的,滿足以下三條簡單規則:
- Derfer 語句中函式的引數是在 defer 語句宣告時確定的。
下面例子中,表示式i的值是在 defer 宣告出確定的。函式返回後,defer 語句輸出為:0。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
複製程式碼
- 當函式返回後,defer 宣告的函式按照後進先出 (Last In First Out) 的順序執行。
下面函式輸出是:3210
func b() {
for i:=0; i < 4; i++ {
defer fmt.print(i)
}
}
複製程式碼
- 如果函式返回的是命名返回值(named return values),defer 宣告的函式呼叫可以讀寫這個返回值。
下面例子中,函式返回後,defer 宣告的函式改變了返回變數 i 的值。因此下面函式返回值為:2
func c() (i int) {
defer func() { i++ }()
return 1
}
複製程式碼
通過這個特性,能方便的修改函式的錯誤返回。後面我們會看到一個簡單的例子。
Panic
Panic 是一個內建 (bulit-in) 函式,能停止函式正常的控制流並進入 panicking 狀態。但某個函式F呼叫了 panic 函式,函式F停止執行,F中宣告的 defer 函式開始執行,之後返回到F的呼叫者。對於呼叫者來說,呼叫F函式就如同呼叫了 panic 函式。該過程繼續向上移動,直到當前goroutine 中的所有函式都返回,此時程式崩潰。Panics 可能源於 panic 函式呼叫,也可能是執行時錯誤,比如陣列越界。
Recover
Recover 是一個內建函式,能將狀態為 panicking 的 goroutine 恢復到正常的控制流。Recover 只能在宣告為 defer 的函式才有用。正常情況下呼叫 recover 函式將返回 nil 且沒有任何效果。如果當前 goroutine 為 panicking 狀態,呼叫 recover 函式將捕獲 panic 的值並且回到正常控制流。
下面例子演示 panic 和 defer 機制:
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接受一個引數 int i
,如果 i 大於 3 則丟擲 panic,否則引數加 1,再次呼叫自己。函式f defer 了一個函式,這個函式 recover 了 panic 然後輸出收到的值(如果不為 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.
複製程式碼
如果移除f中的 defer 函式,那 panic 將不會被捕獲,直到到達當前 goroutine 的呼叫棧頂,程式終止。修改後的輸出為:
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
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
複製程式碼
真實使用 panic 和 recover 的情況,可以檢視 Go 標準庫中 json package 的實現。它使用一些列遞迴函式來解析 JSON。但遇到非法 JSON 時,解析器丟擲 panic,函式棧上面的函式將會捕獲這個 panic,返回一個適當的錯誤值。(檢視 'error' 和 'unmarshal' 方法關於解碼狀態在decode.go中)
Go 庫中的約定是即使包在內部使用 panic,其外部 API 仍然會顯示明確的錯誤返回值。
defer 的其他的用法包括釋放互斥鎖(mutex):
mu.Lock()
defer mu.Unlock()
複製程式碼
列印結尾:
printHeader()
defer printFooter()
複製程式碼
當然還有跟多其他的用法。
總結
總的來說,defer 語句(無論有沒有 panic 或 recover )提供了一個通用和強大的流控制機制。它可用於模擬由其他程式語言中的專用結構實現的許多功能。自己試試看。
譯者總結
- 在傳統的順序、分支(if,switch)、迴圈(for,while)控制流的基礎上,Go 新增了 defer 控制流。defer 控制流類似於 OOP 中的解構函式,它們都為 cleaup 而生,而且執行順序都與正常情況相反(從下到上,從裡到外)。
但 defer 是基於函式的,也就是說每個 defer 語句後面都是一個函式呼叫,如果要實現某些複雜功能,必須得 defer 一個沒有引數和返回值的匿名函式,這種寫法其實很沒有必要,不如寫個語句塊方便。例如:
// 原始 defer 基於函式,下面 defer 一個無引數和返回值的匿名函式,並立即呼叫
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 f() {
defer {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}
...
}
複製程式碼
- Panic 與 Recover 用來丟擲錯誤和捕獲錯誤。值得注意是 Panic 和 Error 的區別,兩者都是程式出現了錯誤,但是 Panic 比 Error 嚴重。當某個錯誤發生,如果這種情況下程式應該立即終止,則用 Panic,不然應該使用 Error。