diff --git a/ch7/ch7-11.md b/ch7/ch7-11.md index 611f23c..a5c5832 100644 --- a/ch7/ch7-11.md +++ b/ch7/ch7-11.md @@ -1,3 +1,66 @@ -## 7.11. 基於類型斷言識别錯誤類型 +## 7.11. 基於類型斷言區别錯誤類型 +思考在os包中文件操作返迴的錯誤集合。I/O可以因爲任何數量的原因失敗,但是有三種經常的錯誤必須進行不同的處理:文件已經存在(對於創建操作),找不到文件(對於讀取操作),和權限拒絶。os包中提供了這三個幫助函數來對給定的錯誤值表示的失敗進行分類: +```go +package os -TODO +func IsExist(err error) bool +func IsNotExist(err error) bool +func IsPermission(err error) bool +``` +對這些判斷的一個缺乏經驗的實現可能會去檢査錯誤消息是否包含了特定的子字符串, +```go +func IsNotExist(err error) bool { + // NOTE: not robust! + return strings.Contains(err.Error(), "file does not exist") +} +``` +但是處理I/O錯誤的邏輯可能一個和另一個平台非常的不同,所以這種方案併不健壯併且對相同的失敗可能會報出各種不同的錯誤消息。在測試的過程中,通過檢査錯誤消息的子字符串來保證特定的函數以期望的方式失敗是非常有用的,但對於線上的代碼是不夠的。 + +一個更可靠的方式是使用一個專門的類型來描述結構化的錯誤。os包中定義了一個PathError類型來描述在文件路徑操作中涉及到的失敗,像Open或者Delete操作,併且定義了一個叫LinkError的變體來描述涉及到兩個文件路徑的操作,像Symlink和Rename。這下面是os.PathError: +```go +package os + +// 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() +} +``` +大多數調用方都不知道PathError併且通過調用錯誤本身的Error方法來統一處理所有的錯誤。盡管PathError的Error方法簡單地把這些字段連接起來生成錯誤消息,PathError的結構保護了內部的錯誤組件。調用方需要使用類型斷言來檢測錯誤的具體類型以便將一種失敗和另一種區分開;具體的類型比字符串可以提供更多的細節。 +```go +_, err := os.Open("/no/such/file") +fmt.Println(err) // "open /no/such/file: No such file or directory" +fmt.Printf("%#v\n", err) +// Output: +// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2} +``` +這就是三個幫助函數是怎麽工作的。例如下面展示的IsNotExist,它會報出是否一個錯誤和syscall.ENOENT(§7.8)或者和有名的錯誤os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一個*PathError,它內部的錯誤是syscall.ENOENT和os.ErrNotExist其中之一。 +```go +import ( + "errors" + "syscall" +) + +var ErrNotExist = errors.New("file does not exist") + +// IsNotExist returns a boolean indicating whether the error is known to +// report that a file or directory does not exist. It is satisfied by +// ErrNotExist as well as some syscall errors. +func IsNotExist(err error) bool { + if pe, ok := err.(*PathError); ok { + err = pe.Err + } + return err == syscall.ENOENT || err == ErrNotExist +} +``` +下面這里是它的實際使用: +```go +_, err := os.Open("/no/such/file") +fmt.Println(os.IsNotExist(err)) // "true" +``` +如果錯誤消息結合成一個更大的字符串,當然PathError的結構就不再爲人所知,例如通過一個對fmt.Errorf函數的調用。區别錯誤通常必須在失敗操作後,錯誤傳迴調用者前進行。 diff --git a/ch7/ch7-12.md b/ch7/ch7-12.md index adf096b..f7a0967 100644 --- a/ch7/ch7-12.md +++ b/ch7/ch7-12.md @@ -1,3 +1,71 @@ -## 7.12. 通過類型斷言査詢接口 +## 7.12. 通過類型斷言詢問行爲 +下面這段邏輯和net/http包中web服務器負責寫入HTTP頭字段(例如:"Content-type:text/html)的部分相似。io.Writer接口類型的變量w代表HTTP響應;寫入它的字節最終被發送到某個人的web瀏覽器上。 +```go +func writeHeader(w io.Writer, contentType string) error { + if _, err := w.Write([]byte("Content-Type: ")); err != nil { + return err + } + if _, err := w.Write([]byte(contentType)); err != nil { + return err + } + // ... +} +``` +因爲Write方法需要傳入一個byte切片而我們希望寫入的值是一個字符串,所以我們需要使用[]byte(...)進行轉換。這個轉換分配內存併且做一個拷貝,但是這個拷貝在轉換後幾乎立馬就被丟棄掉。讓我們假裝這是一個web服務器的核心部分併且我們的性能分析表示這個內存分配使服務器的速度變慢。這里我們可以避免掉內存分配麽? -TODO +這個io.Writer接口告訴我們關於w持有的具體類型的唯一東西:就是可以向它寫入字節切片。如果我們迴顧net/http包中的內幕,我們知道在這個程序中的w變量持有的動態類型也有一個允許字符串高效寫入的WriteString方法;這個方法會避免去分配一個零時的拷貝。(這可能像在黑夜中射擊一樣,但是許多滿足io.Writer接口的重要類型同時也有WriteString方法,包括\*bytes.Buffer,\*os.File和\*bufio.Writer。) + +我們不能對任意io.Writer類型的變量w,假設它也擁有WriteString方法。但是我們可以定義一個隻有這個方法的新接口併且使用類型斷言來檢測是否w的動態類型滿足這個新接口。 +```go +// writeString writes s to w. +// If w has a WriteString method, it is invoked instead of w.Write. +func writeString(w io.Writer, s string) (n int, err error) { + type stringWriter interface { + WriteString(string) (n int, err error) + } + if sw, ok := w.(stringWriter); ok { + return sw.WriteString(s) // avoid a copy + } + return w.Write([]byte(s)) // allocate temporary copy +} + +func writeHeader(w io.Writer, contentType string) error { + if _, err := writeString(w, "Content-Type: "); err != nil { + return err + } + if _, err := writeString(w, contentType); err != nil { + return err + } + // ... +} +``` +爲了避免重複定義,我們將這個檢査移入到一個實用工具函數writeString中,但是它太有用了以致標準庫將它作爲io.WriteString函數提供。這是向一個io.Writer接口寫入字符串的推薦方法。 + +這個例子的神奇之處在於沒有定義了WriteString方法的標準接口和沒有指定它是一個需要行爲的標準接口。而且一個具體類型隻會通過它的方法決定它是否滿足stringWriter接口,而不是任何它和這個接口類型表明的關繫。它的意思就是上面的技術依賴於一個假設;這個假設就是,如果一個類型滿足下面的這個接口,然後WriteString(s)就方法必須和Write([]byte(s))有相同的效果。 +```go +interface { + io.Writer + WriteString(s string) (n int, err error) +} +``` +盡管io.WriteString記録了它的假設,但是調用它的函數極少有可能會去記録它們也做了同樣的假設。定義一個特定類型的方法隱式地獲取了對特定行爲的協約。對於Go語言的新手,特别是那些來自有強類型語言使用背景的新手,可能會發現它缺乏顯式的意圖令人感到混亂,但是在實戰的過程中這幾乎不是一個問題。除了空接口interface{},接口類型很少意外巧合地被實現。 + +上面的writeString函數使用一個類型斷言來知道一個普遍接口類型的值是否滿足一個更加具體的接口類型;併且如果滿足,它會使用這個更具體接口的行爲。這個技術可以被很好的使用不論這個被詢問的接口是一個標準的如io.ReadWriter或者用戶定義的如stringWriter。 + +這也是fmt.Fprintf函數怎麽從其它所有值中區分滿足error或者fmt.Stringer接口的值。在fmt.Fprintf內部,有一個將單個操作對象轉換成一個字符串的步驟,像下面這樣: +```go +package fmt + +func formatOneValue(x interface{}) string { + if err, ok := x.(error); ok { + return err.Error() + } + if str, ok := x.(Stringer); ok { + return str.String() + } + // ...all other types... +} +``` +如果x滿足這個兩個接口類型中的一個,具體滿足的接口決定對值的格式化方式。如果都不滿足,默認的case或多或少會統一地使用反射來處理所有的其它類型;我們可以在第12章知道具體是怎麽實現的。 + +再一次的,它假設任何有String方法的類型滿足fmt.Stringer中約定的行爲,這個行爲會返迴一個適合打印的字符串。