1. 程式人生 > >Go語言之異常處理

Go語言之異常處理

在編寫Go語言程式碼的時候,我們應該習慣使用error型別值來表明非正常的狀態。作為慣用法,在Go語言標準庫程式碼包中的很多函式和方法也會以返回error型別值來表明錯誤狀態及其詳細資訊。

error是一個預定義識別符號,它代表了一個Go語言內建的介面型別。這個介面的型別宣告如下:

type error interface{
	Error() string
}
其中的Error方法宣告的意義就在於為方法呼叫方提供當前錯誤狀態的詳細資訊。任何資料型別只要實現了這個可以返回string型別值的Error方法就可以成為一個error介面型別的實現。不過在通常情況下,我們並不需要自己去編寫一個error的實現型別。Go語言的標準庫程式碼包errors為我們提供了一個用於建立errors型別值的函式New。該方法的宣告如下:
func New(text string) error {
	return &errorString{text}
}
errors.New函式接受一個string型別的引數值並可以返回一個error型別值。這個error型別值的動態型別就是errors.errorString型別。New函式的唯一引數被用於初始化那個errors.errorString型別的值。從代表這個實現型別的名稱上可以看出,該型別是一個包級私有的型別。它只是errors包的內部實現的一部分,而非公開的API。errors.errorString型別及其方法的宣告如下:
<pre style="margin-top: 0px; margin-bottom: 0px;"><span style="font-weight: 600; color: rgb(0, 0, 128);">type</span><span style=" color:#c0c0c0;"> </span><span style=" color:#000080;">errorString</span><span style=" color:#c0c0c0;"> </span><span style=" font-weight:600; color:#000080;">struct</span><span style=" color:#c0c0c0;"> </span>{
sstring
}
func(e*errorString)Error()string{
returne.s
}
傳遞給errors.New函式的引數值就是當我們呼叫它的Error方法的時候返回的那個結果值。

我們可以使用程式碼包fmt中的列印函式打印出error型別值所代表的錯誤的詳細資訊,就像這樣:

var err error = errors.New("A normal error.")
這些列印函式在發現列印的內容是一個error型別值的時候都會呼叫該值的Error方法並將結果值作為該值的字串表示形式。因此,我們傳遞給errors.New的引數值即是其返回的error型別值的字串表示形式。

另一個可以生成error型別值的方法是呼叫fmt包中的Errorf函式。呼叫它的程式碼類似於:

err2:=fmt.Errorf("%s\n","Anormalerror.")

與fmt.Printf函式相同,fmt.Errorf函式可以根據格式說明符和後續引數生成一個字串型別值。但與fmt.Printf函式不同的是,fmt.Errorf函式並不會在標準輸出上列印這個生成的字串型別值,而是用它來初始化一個error型別值並作為該函式的結果值返回給呼叫方。在fmt.Errorf函式的內部,建立和初始化error型別值的操作正是通過呼叫errors.New函式來完成。

在大多數情況下,errors.New函式和fmt.Errorf函式足以滿足我們建立error型別值的要求。但是,介面型別error使得我們擁有了很大的擴充套件空間。我們可以根據需要定義自己的error型別。例如,我們可以使用額外的欄位和方法讓程式使用方能夠獲取更多的錯誤資訊。例如,結構體型別os.PathError是一個error介面型別的實現型別。它的宣告中包含了3個欄位,這使得我們能夠從它的Error方法的結果值當中獲取到更多的資訊。os.PathError型別及其方法的宣告如下:

// PathError records an error and the operation and file path that caused it.
type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
os.PathError型別的宣告上我們可以獲知,它的3個欄位都是公開的。因此,在任何位置上我們都可以直接通過選擇符訪問到它們。但是,在通常情況下,函式或方法中的相關結果宣告的型別應該是error型別,而不應該是某一個error型別的實現型別。這也是為了遵循面向介面程式設計的原則。在這種情況下,我們常常需要先判定獲取到的error型別值的動態型別,再依此來進行必要的型別轉換和後續操作。例如:
<span style="white-space:pre">	</span>file, err3 := os.Open("/etc/profile")
	if err3 != nil {
		if pe, ok := err3.(*os.PathError); ok {
			fmt.Printf("Path Error: %s (op=%s, path=%s)\n", pe.Err, pe.Op, pe.Path)
		} else {
			fmt.Printf("Unknown Error: %s\n", err3)
		}
	}
我們通過型別斷言表示式和if語句來對os.Open函式返回的error型別值進行處理。這與把error型別值作為結果值來表達函式執行的錯誤狀態的做法一樣,也屬於Go語言中的異常處理的慣用法之一。

如果os.Open函式在執行過程中沒有發生任何錯誤,那麼我們就可以對變數file所代表的檔案的內容進行讀取了。相關程式碼如下:

r := bufio.NewReader(file)
	var buf bytes.Buffer

	for {
		byteArray, _, err4 := r.ReadLine()
		if err4 != nil {
			if err4 == io.EOF {
				break
			} else {
				fmt.Printf("Read Error: %s\n", err4)
				break
			}
		} else {
			buf.Write(byteArray)
		}
	}
io.EOF變數正是由errors.New函式的結果值來初始化的。EOF是檔案結束符(End Of File)的縮寫。對於檔案讀取操作來說,它意味著讀取器已經讀到了檔案的末尾。因此,嚴格來說,EOF並不應該算作一個真正的錯誤,而僅僅屬於一種“錯誤訊號”。

變數r代表了一個讀取器。它的ReadLine方法返回3個結果值。第三個結果值的型別就是error型別的。當讀取器讀到file所代表的檔案的末尾時,ReadLine方法會直接將變數io.EOF的值作為它的第三個結果值返回。如果判斷的結果為true,那麼我們就可以直接終止那個用於連續讀取檔案內容的for語句的執行。否則,我們就應該意識到在讀取檔案內容的過程中有真正的錯誤發生了,並採取相應的措施。

注意,只有當兩個error型別的變數的值確實為同一個值的時候,使用比較操作符==進行判斷時才會得到true。從另一個角度看,我們可以預先宣告一些error型別的變數,並把它們作為特殊的“錯誤訊號”來使用。任何需要返回同一類“錯誤訊號”的函式或方法都可以直接把這類預先宣告的值拿來使用。這樣我們就可以很便捷的使用==來識別這些“錯誤訊號”並進行相應的操作了。

不過,需要注意的是,這類變數的值必須是不可變的。也就是說,它們的實際型別的宣告中不應該包含任何公開的欄位,並且附屬於這些型別的方法也不應該包含對其欄位進行賦值的語句。例如,我們前面提到的os.PathError型別就不適合作為這類變數的值的動態型別,否則很可能會造成不可預知的後果。

這種通過預先宣告error型別的變數為程式使用方提供便利的做法在Go語言標準庫程式碼包中非常常見。

關於實現error介面型別的另一個技巧是,我們還可以通過把error介面型別嵌入到新的介面型別中對它進行擴充套件。例如,標準庫程式碼包net中的Error介面型別,其宣告如下:

<span style="font-size:18px;">// An Error represents a network error.
type Error interface {
	error
	Timeout() bool   // Is the error a timeout?
	Temporary() bool // Is the error temporary?
}</span>
一些在net包中宣告的函式會返回動態型別為net.Error的error型別值。在使用方,對這種error型別值的動態型別的判定方法與前面提及的基本一致。

如果變數err的動態型別是net.Error,那麼就我們可以根據它的Temporary方法的結果值來判斷當前的錯誤狀態是否臨時的:

if netErr, ok := err.(net.Error); ok && netErr.Temporary() {<span style="white-space:pre">	</span>}
如果是臨時的,那麼就可以間隔一段時間之後再進行對之前的操作進行重試,否則就記錄錯誤狀態的資訊並退出。假如我們沒有對這個error型別值進行型別斷言,也就無法獲取到當前錯誤狀態的那個額外屬性,更無法決定是否應該進行重試操作了。這種對error型別的無縫擴充套件方式所帶來的益處是顯而易見的。

在Go語言中,對錯誤的正確處理是非常重要的。語言本身的設計和標準庫程式碼中展示的慣用法鼓勵我們對發生的錯誤進行顯式的檢查。雖然這會使Go語言程式碼看起來稍顯冗長,但是我們可以使用一些技巧來簡化它們。這些技巧大都與通用的程式設計最佳實踐大同小異,或者已經或將要包含在我們所講的內容(自定義錯誤型別、單一職責函式等)中,所以這並不是問題。況且,這一點點代價比傳統的try-catch方式帶來的弊端要小得多。