- 原文標題:Defer, Panic, and Recover
- 原文做者:Andrew Gerrand
- 原文時間:2010-08-04
Go 語言有通用的控制流機制:if
, for
, switch
, goto
。還有 go
語句將代碼運行在不一樣的 goroutine 中。這裏我要討論下幾個不是那麼經常使用的控制流:defer
, panic
和 recover
。golang
一個 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 語言,咱們老是能保證打開的文件能關閉:json
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 語句的行爲是很是簡單和可預測的,知足如下三條簡單規則:數組
下面例子中,表達式i的值是在 defer 聲明出肯定的。函數返回後,defer 語句輸出爲:0。bash
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
複製代碼
下面函數輸出是:3210編程語言
func b() {
for i:=0; i < 4; i++ {
defer fmt.print(i)
}
}
複製代碼
下面例子中,函數返回後,defer 聲明的函數改變了返回變量 i 的值。所以下面函數返回值爲:2函數
func c() (i int) {
defer func() { i++ }()
return 1
}
複製代碼
經過這個特性,能方便的修改函數的錯誤返回。後面咱們會看到一個簡單的例子。ui
Panic 是一個內建 (bulit-in) 函數,能中止函數正常的控制流並進入 panicking 狀態。但某個函數F調用了 panic 函數,函數F中止執行,F中聲明的 defer 函數開始執行,以後返回到F的調用者。對於調用者來講,調用F函數就如同調用了 panic 函數。該過程繼續向上移動,直到當前goroutine 中的全部函數都返回,此時程序崩潰。Panics 可能源於 panic 函數調用,也多是運行時錯誤,好比數組越界。spa
Recover 是一個內建函數,能將狀態爲 panicking 的 goroutine 恢復到正常的控制流。Recover 只能在聲明爲 defer 的函數纔有用。正常狀況下調用 recover 函數將返回 nil 且沒有任何效果。若是當前 goroutine 爲 panicking 狀態,調用 recover 函數將捕獲 panic 的值而且回到正常控制流。
下面例子演示 panic 和 defer 機制:code
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 )提供了一個通用和強大的流控制機制。它可用於模擬由其餘編程語言中的專用結構實現的許多功能。本身試試看。
// 原始 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)
}
}
...
}
複製代碼