[TOC]
向調用者返回某種形式的錯誤信息是庫歷程必須提供的一項功能。通過前面介紹的函數多返回值的特性,Go中的錯誤信息可以很容易同正常情況下的返回值一起返回給調用者。方便起見,錯誤通常都用內置接口`error`類型表示。
~~~
type error interface {
Error() string
}
~~~
庫開發人員可以通過實現該接口來豐富其內部功能,使其不僅能夠呈現錯誤本身,還能提供更多的上下文信息。舉例來說,`os.Open`函數會返回`os.PathError`錯誤。
~~~
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
~~~
`PathError`的`Error`方法會生成類似下面給出的錯誤信息:
~~~
open /etc/passwx: no such file or directory
~~~
這條錯誤信息包括了足夠的信息:出現異常的文件名,操作類型,以及操作系統返回的錯誤信息等,因此即使它冒出來的時候距離真正錯誤發生時刻已經間隔了很 久,也不會給調試分析帶來很大困難,比直接輸出一句“no such file or directory” 要友好的多。
如果可能,描述錯誤的字符串應該能指明錯誤發生的原始位置,比如在前面加上一些諸如操作名稱或包名稱的前綴信息。例如在`image`包中,用來輸出未知圖片類型的錯誤信息的格式是這樣的:“image: unknown format” 。
對于需要精確分析錯誤信息的調用者,可以通過類型開關或類型斷言的方式查看具體的錯誤并深入錯誤的細節。就`PathErrors`類型而言,這些細節信息包含在一個內部的`Err`字段中,可以被用來進行錯誤恢復。
~~~
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
~~~
在上面例子中,第二個`if`語句是另一種形式的[類型斷言](http://www.hellogcc.org/effective_go.html#interface_conversions)。如該斷言失敗,`ok`的值將為false且`e`的值為`nil`。如果斷言成功,則`ok`值為true,說明當前的錯誤,也就是`e`,屬于`*os.PathError`類型,因而可以進一步獲取更多的細節信息。
## 嚴重故障(Panic)
通常來說,向調用者報告錯誤的方式就是返回一個額外的`error`變量:?`Read`方法就是一個很好的例子;該方法返回一個字節計數值和一個`error`變量。但是對于那些不可恢復的錯誤,比如錯誤發生后程序將不能繼續執行的情況,該如何處理呢?
為了解決上述問題,Go語言提供了一個內置的`panic`方法,用來創建一個運行時錯誤并結束當前程序(關于退出機制,下一節還有進一步介紹)。該函數接受一個任意類型的參數,并在程序掛掉之前打印該參數內容,通常我們會選擇一個字符串作為參數。方法`panic`還適用于指示一些程序中的不可達狀態,比如從一個無限循環中退出。
~~~
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
~~~
以上僅僅提供一個應用的示例,在實際的庫設計中,應盡量避免使用`panic`。如果程序錯誤可以以某種方式掩蓋或是繞過,那么最好還是繼續執行而不是讓整個程序終止。不過還是有一些反例的,比方說,如果庫歷程確實沒有辦法正確完成其初始化過程,那么觸發`panic`退出可能就是一種更加合理的方式。
~~~
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
~~~
## 恢復(Recover)
對于一些隱式的運行時錯誤,如切片索引越界、類型斷言錯誤等情形下,`panic`方法就會被調用,它將立刻中斷當前函數的執行,并展開當前Goroutine的調用棧,依次執行之前注冊的defer函數。當棧展開操作達到該Goroutine棧頂端時,程序將終止。但這時仍然可以使用Go的內建`recover`方法重新獲得Goroutine的控制權,并將程序恢復到正常執行的狀態。
調用`recover`方法會終止棧展開操作并返回之前傳遞給`panic`方法的那個參數。由于在棧展開過程中,只有defer型函數會被執行,因此`recover`的調用必須置于defer函數內才有效。
在下面的示例應用中,調用`recover`方法會終止server中失敗的那個Goroutine,但server中其它的Goroutine將繼續執行,不受影響。
~~~
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
~~~
在這里例子中,如果`do(work)`調用發生了panic,則其結果將被記錄且發生錯誤的那個Goroutine將干凈的退出,不會干擾其他Goroutine。你不需要在defer指示的閉包中做別的操作,僅需調用`recover`方法,它將幫你搞定一切。
只有直接在defer函數中調用`recover`方法,才會返回非`nil`的值,因此defer函數的代碼可以調用那些本身使用了`panic`和`recover`的庫函數而不會引發錯誤。還用上面的那個例子說明:`safelyDo`里的defer函數在調用`recover`之前可能調用了一個日志記錄函數,而日志記錄程序的執行將不受panic狀態的影響。
有了錯誤恢復的模式,`do`函數及其調用的代碼可以通過調用`panic`方法,以一種很干凈的方式從錯誤狀態中恢復。我們可以使用該特性為那些復雜的軟件實現更加簡潔的錯誤處理代碼。讓我們來看下面這個例子,它是`regexp`包的一個簡化版本,它通過調用`panic`并傳遞一個局部錯誤類型來報告“解析錯誤”(Parse Error)。下面的代碼包括了`Error`類型定義,`error`處理方法以及`Compile`函數:
~~~
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
~~~
如果`doParse`方法觸發panic,錯誤恢復代碼會將返回值置為`nil`—因為defer函數可以修改命名的返回值變量;然后,錯誤恢復代碼會對返回的錯誤類型進行類型斷言,判斷其是否屬于`Error`類型。如果類型斷言失敗,則會引發運行時錯誤,并繼續進行棧展開,最后終止程序 —— 這個過程將不再會被中斷。類型檢查失敗可能意味著程序中還有其他部分觸發了panic,如果某處存在索引越界訪問等,因此,即使我們已經使用了`panic`和`recover`機制來處理解析錯誤,程序依然會異常終止。
有了上面的錯誤處理過程,調用`error`方法(由于它是一個類型的綁定的方法,因而即使與內建類型`error`同名,也不會帶來什么問題,甚至是一直更加自然的用法)使得“解析錯誤”的報告更加方便,無需費心去考慮手工處理棧展開過程的復雜問題。
~~~
if pos == 0 {
re.error("'*' illegal at start of expression")
}
~~~
上面這種模式的妙處在于,它完全被封裝在模塊的內部,`Parse`方法將其內部對`panic`的調用隱藏在`error`之中;而不會將`panics`信息暴露給外部使用者。這是一個設計良好且值得學習的編程技巧。
順便說一下,當確實有錯誤發生時,我們習慣采取的“重新觸發panic”(re-panic)的方法會改變panic的值。但新舊錯誤信息都會出現在崩潰 報告中,引發錯誤的原始點仍然可以找到。所以,通常這種簡單的重新觸發panic的機制就足夠了—所有這些錯誤最終導致了程序的崩潰—但是如果只想顯示最 初的錯誤信息的話,你就需要稍微多寫一些代碼來過濾掉那些由重新觸發引入的多余信息。這個功能就留給讀者自己去實現吧!