Go 有通用的控制流程:if,for,switch,goto。它也有go語句用於讓程式碼執行在單獨的協程。這裡我將討論一些不常見的問題:defer,panic 和 recover。
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
}
這個能工作,但有一個漏洞。如果呼叫os.Create
失敗,函式將在不關閉原始檔的情況下返回。這可以輕鬆補救,在第二個return
語句之前呼叫src.Close
,但如果函式更復雜,則問題可能不那麼容易被注意到和解決。通過引入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
被宣告時,其引數就會被計算。
在此示例中,當Println
呼叫被defer宣告,將計算表示式“i”。defer
呼叫將在函式返回後列印“0”。
2、defer
的執行順序為先進後出。
此函式列印“3210”:
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
3、defer
可以讀取有名返回值。
在此示例中,defer函式在函式返回後遞增返回值i
。因此,此函式返回2:
func c() (i int) {
defer func() { i++ }()
return 1
}
這對於修改函式的錯誤返回值很方便;我們很快就會看到一個這樣的例子。
Panic
是一個內建功能,可以停止普通的控制流程並開始Panicing
。當函式F
呼叫panic
時,F
的執行會停止,F
中的任何defer
函式都正常執行,然後F
返回給其呼叫方。對於呼叫方,F
的表現就像是panic
。該過程繼續在堆疊中向上移動,直到當前協程 中的所有函式都返回,此時程式崩潰。可以通過直接呼叫panic
來啟動panic
。它們也可能是由執行時錯誤引起的,例如越界陣列訪問。
recover
是一個內建功能,可以重新獲得對正在panic
的協程的控制。recover
僅在defer
函式中有用。在正常執行期間,recover
呼叫將返回nil
,並且沒有其他效果。如果當前協程 出現panic
,則呼叫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
,否則它將使用引數i+1
呼叫自己。函式f
defer
呼叫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.
如果我們從f
中刪除derfer
函式,則不會恢復panic
,並在到達協程呼叫堆疊頂部時終止程式。這個修改後的程式將輸出:
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 包。它使用一組遞迴函式對介面進行編碼。如果在遍歷該值時發生錯誤,則會呼叫panic
將堆疊展開到最上層的函式呼叫,該呼叫將從panic
中恢復並返回適當的錯誤值(請參閱 encode.go 中 encodeState 型別的“error”和“marshal”方法)。
Go
庫中的約定是,即使包在內部使用panic
,其對外的API
仍會展示顯式錯誤返回值。
defer
的其他用法(除了前面給出的file.Close
例子)還包括釋放互斥鎖:
mu.Lock()
defer mu.Unlock()
列印頁尾:
printHeader()
defer printFooter()
以及更多。
總之,defer
語句(有/沒有panic
和recover
)為控制流提供了一種不同尋常且功能強大的機制。它可用於對其他程式語言中特殊用途結構實現的許多特性進行建模。試試吧。