Upspin 中的錯誤處理 —— 來自 Rob Pike

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。[Upspin](https://upspin.io/) 項目使用自訂的包 —— [upspin.io/errors](https://godoc.org/upspin.io/errors) —— 來表示系統內部出現的錯誤條件。這些錯誤滿足標準的 Go [error](https://golang.org/pkg/builtin/#error) 介面,但是使用的是自訂類型 [upspin.io/errors.Error](https://godoc.org/upspin.io/errors#Error),該類型具有一些已經證明對項目有用的屬性。這裡,我們會示範這個包是如何工作的,以及如何使用這個包。這個故事為關於 Go 中的錯誤處理更廣泛的討論提供了經驗教訓。## 動機在項目進行幾個月後,我們清楚地知道,我們需要一致的方法來處理整個代碼中的錯誤構建、描述和處理。我們決定實現一個自訂的 errors 包,並在某個下午將其推出。相較於初始實現,細節已經有所變化,但是,這個包背後的基本理念經久不衰。它們是:* 為了便於構建有用的錯誤資訊。* 為了使使用者易於理解錯誤。* 為了讓錯誤協助程式員進行問題診斷。隨著我們開發此包的經驗增長,出現了一些其他的需求。下面我們會聊到這些。## errors 包之旅[upspin.io/errors](https://godoc.org/upspin.io/errors) 包是用包名 “errors” 匯入的,所以,在 Upspin 中,它取代了 Go 標準的 “errors” 包。我們注意到,Upspin 中的錯誤資訊的元素都是不同類型的:使用者名稱、路徑名、錯誤種類(I/O、Permission 等等),諸如此類。這為 errors 包提供了起始點,它將建立在這些不同類型之上,以構建、表示和報告出現的錯誤。這個包的中心是 [Error](https://godoc.org/upspin.io/errors#Error) 類型,這是一個 Upspin 錯誤的具體表示。它具有多個欄位,任何一個欄位都可以不做設定:```go type Error struct { Path upspin.PathName User upspin.UserName Op Op Kind Kind Err error}```Path 和 User 欄位表示操作影響的路徑和使用者。注意,這些都是字串,但是分別為 Upspin 中專屬的類型,以表明其用途,並且使得類型系統可以捕獲到某些類型的編程錯誤。Op 欄位表示執行的操作。它是另一種字串類型,通常儲存方法名或者報告錯誤的伺服器函數名稱:“client.Lookup”、“dir/server.Glob” 等等。Kind 欄位把錯誤分類為一組標準條件(Permission、IO、NotExist,[諸如此類](https://godoc.org/upspin.io/errors#Kind))中的一員。這使得我們很容易就可以看到出現的錯誤的類型的簡潔描述,並且還提供了串連到其他系統的鉤子。例如,[upspinfs](https://godoc.org/upspin.io/cmd/upspinfs) 把 Kind 欄位當成將 Upspin 錯誤轉換成 Unix 錯誤常量(例如 EPERM 和 EIO)的鍵來使用。最後一個欄位,Err,儲存另一個錯誤值。通常是來自其他系統的錯誤,例如 [os](https://golang.org/pkg/os/) 包的檔案系統錯誤,或者 [net](https://golang.org/pkg/net/) 包的網路錯誤。它也有可能是另一個 upspin.io/errors.Error 值,用以建立錯誤跟蹤(稍後我們會討論)。## 構建錯誤為了協助錯誤構建,這個包提供了一個名為 [E](https://godoc.org/upspin.io/errors#E) 的函數,它簡短並且便於輸入。```gofunc E(args ...interface{}) error```如該函數的[文檔注釋](https://godoc.org/upspin.io/errors#E)所述,E 根據其輸入參數構建 error 值。每一個參數的類型決定了其自身的含義。思想是檢查每一個參數的類型,然後將參數值賦給已構造的 Error 結構中對應類型的欄位。這裡有一個明顯的對應點:PathName 對應 Error.Path,UserName 對應 Error.User,以此類推。讓我們看一個例子。通常情況下,一個方法中會出現多次對 errors.E 的調用,因此,我們定義一個常量,按慣例稱其為 op,它會作為參數傳給方法中所有 E 調用:```gofunc (s *Server) Delete(ref upspin.Reference) error { const op errors.Op = "server.Delete" ...```然後,在整個方法中,我們都會把這個常量作為每一次 E 調用的第一個參數(雖然參數的實際順序是不相干的,但是按慣例,op 放在第一個):```goif err := authorize(user); err != nil { return errors.E(op, user, errors.Permission, err)}```E 的 String 方法會將其整潔地格式化:```server.Delete: user ann@example.com: permission denied: user not authorized ```如果錯誤多級嵌套,那麼會抑制冗餘欄位,並且使用縮排來格式化嵌套:```client.Lookup: ann@example.com/file: item does not exist: dir/remote("upspin.example.net:443").Lookup: dir/server.Lookup ```注意,這條錯誤資訊中提到了多種操作(client.Lookup,dir/remote,dir/server)。在後面的部分,我們會討論這種多重性。又如,有時,錯誤是特殊的,並且在調用處通過一個普通的字串來清楚描述。為了以明顯的方式使其行之有效,構造器通過類似於標準的 Go 函數 [errors.New](https://golang.org/pkg/errors/#New) 的機制,將文字類型字串參數轉換成 Go error 類型。因此,可以這樣寫:```goerrors.E(op, "unexpected failure")```或者```goerrors.E(op, fmt.Sprintf("could not succeed after %d tries", nTries))```這樣,會讓字串賦給結果 Err 類型的 Err 欄位。這是構建特殊錯誤的一種自然而然的簡單方式。## 跨網路錯誤Upspin 是一個分布式系統,因此,Upspin 伺服器之間的通訊保留錯誤的結構則是至關重要的。為了做到這一點,我們使用 errors 包的 [MarshalError](https://godoc.org/upspin.io/errors#MarshalError) 和 [UnmarshalError](https://godoc.org/upspin.io/errors#UnmarshalError) 函數來在網路連接中轉碼錯誤,從而讓 Upspin 的 RPC 知道這些錯誤類型。這些函數確保用戶端將看到伺服器在構造錯誤時提供的所有細節。考慮下面的錯誤報表:```client.Lookup: ann@example.com/test/file: item does not exist: dir/remote("dir.example.com:443").Lookup: dir/server.Lookup: store/remote("store.example.com:443").Get: fetching https://storage.googleapis.com/bucket/C1AF...: 404 Not Found```它由四個嵌套的 errors.E 值構成。從下往上看,最裡面的部分來自於包 [upspin.io/store/remote](http://upspin.io/store/remotehttps://godoc.org/upspin.io/store/remote) (負責與遠程儲存伺服器互動)。這個錯誤表示,在從儲存擷取對象時出現問題。該錯誤大概是這樣構建的,封裝了來自雲儲存提供者的一個底層錯誤:```goconst op errors.Op = `store/remote("store.example.com:443").Get`var resp *http.Response...return errors.E(op, errors.Sprintf("fetching %s: %s", url, resp.Status))```下一個錯誤來自目錄伺服器(包 [upspin.io/dir/server](https://godoc.org/upspin.io/dir/server),我們的目錄伺服器參考實現),它表示目錄伺服器在錯誤發生時正在嘗試進行尋找操作。這個錯誤是像這樣構建的:```goconst op errors.Op = "dir/server.Lookup"...return errors.E(op, pathName, errors.NotExist, err)```這是第一層,其中,增加了一個 Kind(errors.NotExist)。Lookup 錯誤值通過網路傳遞(一路上被打包和解包),接著,[upspin.io/dir/remote](https://godoc.org/upspin.io/dir/remote) 包(負責跟遠程目錄伺服器互動)通過它自己對 errors.E 的調用來封裝這個錯誤:```go const op errors.Op = "dir/remote.Lookup" ... return errors.E(op, pathName, err)```在這個調用中,沒有設定任何 Kind,因此,在構建這個 Error 結構時,使用內部的 Kind(errors.NotExist)。最終,[upspin.io/client](https://godoc.org/upspin.io/client) 包再一次封裝這個錯誤:```goconst op errors.Op = "client.Lookup"...return errors.E(op, pathName, err)```保留伺服器錯誤結構使得用戶端能夠以編程的方式知道這是一個 “not exist” 錯誤,以及問題的相關項目是 “ann@example.com/file”。錯誤的 [Error](https://godoc.org/upspin.io/errors#Error.Error) 方法可以利用這個結構來抑制冗餘欄位。如果伺服器錯誤只是一個含糊不清的字串,那麼我們會在輸出中多次看到路徑名。關鍵細節(PathName 和 Kind)被拉到錯誤的頂部,這樣的話,在展示中它們會更突出。期望是,當使用者看到這些錯誤時,錯誤的第一行通常就夠了;當需要進一步的診斷的時候,下面的細節會更有用。我們回過頭來把錯誤展示作為一個整體,我們可以通過各種網路連接組件,從錯誤的產生一直追蹤到用戶端。完整的錯誤鏈也許會協助到使用者,但它是一定能幫到系統的實現者的,這能協助他們確定問題是不是意料之外的,或者是不是非同尋常的。## 使用者和實現者讓錯誤對終端使用者有用並且保持簡潔,與讓錯誤對實現者而言資訊豐富並且可供分析,二者之間存在矛盾。常常是實現者勝出,而錯誤變得過於冗餘,達到了包含堆疊追蹤或者其他淹沒式細節的程度。Upspin 的錯誤試圖讓使用者和實現者都滿意。報告的錯誤適度簡潔,關注於使用者應該覺得有用的資訊。但它們還包含內部詳細資料,例如方法實現者可以擷取診斷資訊,但又不會把使用者淹沒。在實踐中,我們發現這種權衡工作良好。相反,類似於堆疊追蹤的錯誤在這兩方面上都更糟糕。使用者沒有上下文可以理解堆疊追蹤,而如果服務端錯誤被傳給用戶端的話,那麼看到堆疊追蹤的實現者會很難看到應該出現的資訊。這就是為什麼 Upspin 錯誤嵌套相當於_操作_跟蹤(顯示系統元素路徑),而不是_執行_跟蹤(顯示代碼執行路徑)。這個區別至關重要。對於那些堆疊追蹤可能會有用的情境,我們允許使用 “debug” 標籤來構建 errors 包,這將會允許列印堆疊追蹤。這個工作良好,但是值得注意的是,我們幾乎從不使用這個功能。相反,errors 包的預設行為已經夠好了,避免了堆疊追蹤的開銷和不堪入目。## 匹配錯誤Upspin 的自訂錯誤處理的一個意想不到的好處是,易於編寫錯誤依賴的測試以及編寫測試之外的錯誤敏感代碼。errors 包中的兩個函數使得這些用法成為可能。首先是一個函數,名為 [errors.Is](https://godoc.org/upspin.io/errors#Is),它返回一個布爾值,表明參數 err 是否是 *errors.Error 類型,如果是,那麼它的 Kind 欄位有特定的值。```gofunc Is(kind Kind, err error) bool```這個函數使得代碼可以根據錯誤條件直接改變行為,例如,在面對許可權錯誤時與網路錯誤不同:```goif errors.Is(errors.Permission, err) { ... }```另一個函數, [Match](https://godoc.org/upspin.io/errors#Match),對測試有用。在我們已經使用 errors 包一段時間,然後發現我們太多的測試是對錯誤細節敏感時,於是建立了它。例如,一個測試可能只需要檢查是否存在開啟特定檔案的許可權錯誤,但對錯誤資訊的準確格式很敏感。在修複了許多像這樣的脆弱的測試之後,我們編寫了一個函數來報告接收到的錯誤 err 是否匹配一個錯誤模板 (template):```gofunc Match(template, err error) bool```這個函數檢查錯誤是否是 *errors.Error 類型的,如果是,那麼錯誤中的欄位是否與模板中的那些欄位相等。關鍵是,它_只_檢查模板中的那些非零欄位,忽略其他欄位。對於上述例子,我們可以這樣寫:```goif errors.Match(errors.E(errors.Permission, pathName), err) { … }```並且不會受到該錯誤的其他屬性影響。在我們的測試中,我們無數次使用 Match;它就是一個大驚喜。## 經驗教訓在 Go 社區中,有大量關於如何處理錯誤的討論,重要的是,要意識到這個問題並沒有單一的答案。沒有一個包或者是一個方法可以滿足所有程式的需求。正如[這裡](https://blog.golang.org/errors-are-values)指出的,錯誤只是值,並且可以以不同的方式編程,從而滿足不同的情境。Upspin 的 errors 包對我們有好處。我們並非主張對於另一個系統,它就是正確的答案,或者甚至說這個方法適合其他人。但是這個包在 Upspin 中用得不錯,並且教會我們一些值得記錄的經驗教訓。Upspin 的 errors 包的大小和涵蓋的範圍適度。其初始實現是在幾個小時內完成的,而基本的設計保留了下來,並且自完成後,經曆了一些改進。為另一個項目定製一個錯誤包應該也很容易。應該很容易適用於任何特定環境的具體需求。不要害怕嘗試;只需先想一下,並且願意嘗試。當考慮你自己的項目的細節時,思考現在有什麼可以改進的地方。我們確保錯誤構造器便於使用,並且易於閱讀。如果並非如此,那麼編程人員可以拒絕使用。errors 包的行為一定程度建立在底層系統內部的類型上的。這是一個很小但是很重要的點:沒有哪個一般的錯誤包可以做到我們做到的東西。它真的是一個自訂包。此外,區別參數的類型的使用使得錯誤構建變得通順流暢。這個可以通過組合系統中現有的類型(PathName、UserName)和為該目的而建立的新類型(Op、Kind)來實現。協助式類型使得錯誤構建乾淨、安全並且容易。它花費一點額外的工作量(我們必須建立這些類型,然後處處使用它們,例如通過 “const op”),但結果是值得的。最後,我們想要強調,缺乏堆疊追蹤是 Upspin 中的錯誤模型的一部分。相反,errors 包報告事件序列(通常跨網路),這樣子產生的是傳遞給用戶端的錯誤。通過系統中的操作小心構造錯誤可以比簡單的堆疊追蹤更簡潔、更具描述性以及更有用。錯誤是給使用者的,而不只是給程式員的。(Errors are for users, not just for programmers.)_來自 Rob Pike 和 Andrew Gerrand_

via: https://commandcenter.blogspot.co.uk/2017/12/error-handling-in-upspin.html

作者:Rob Pike 譯者:ictar 校對:rxcai polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

660 次點擊  ∙  1 贊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.