Go 進階訓練營 Week02: error 錯誤處理


Error vs Exception

Error:

Go error 就是普通的一個接口,普通的值。Errors are values

type error interface {
    Error() string
}

經常使用 errors.New() 來返回一個 error 對象,errors.New() 返回的是內部 errorString 對象的指針

errors.go 中

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

為什么返回的是 errorString 對象的指針而不是值呢?

我們看一個demo

package main

import (
  "errors"
  "fmt"
)

// Create a named type for our new error type.
type errorString string

// Implement the error interface
func (e errorString) Error() string {
   return string(e)
}

// New creates interface values of type error.
func New(text string) error {
   return errorString(text)
}

var ErrNamedType = New("EOF")
var ErrStructType = errors.New("EOF")

func main() {
    if ErrNamedType == New("EOF") {
        fmt.Println("Named Type Error")
     }

    if ErrStructType == errors.New("EOF") {
        fmt.Println("Struct Type Error")
    }
}

在對比兩個struct 是否相同的時候,會去對比,這兩個 struct 里面的各個字段是否是相同的,如果相同就返回true,但是對比指針的時候回去判斷兩個指針的地址是否一致。

Go 的異常處理

Go的處理異常邏輯是不引入 exception,支持多參數返回。如果一個函數返回了 value,error, 你不能對 value 做任何的假設,必須先判定 error。唯一可以忽略掉 error 的是,如果你連 value 也不關心。

you only need to check the error value if you care about eh result. --Dave

Go 中有panic的機制。與其他語言不同,當我們拋出異常的時候,相當於把exception 扔給了調用者來處理。

Go panic 意味着 fatal error(就是掛了)。不能假設調用者來解決panic,意味着代碼不能繼續運行。

使用多個返回值和一個簡單的約定,Go解決了讓程序員知道什么時候出了問題,並為真正的異常情況保留了panic.

Go Error 的一些思想

簡單

考慮失敗,而不是成功(Plan for failure,not success).

沒有隱藏的控制流

完全交給你來控制 error

Error are values

Error Type

Sentinel Error

預定義的特定錯誤,我們叫為 sentinel error, 這個名字來源於計算機編程中使用一個特定值來表示不可能進行進一步處理的做法。

ErrSomething = errors.New("xxx")

if err == ErrSomething { ... }

Sentinel Error 的不足

  1. Sentinel errors 成為你API 公共部分,增加了API的表面積。

  2. Sentinel errors 在兩個包之間創建了依賴

如:檢查錯誤是否等於 io.EOF,代碼必須導入 io包。

結論:所以盡可能避免 sentinel errors

Error types

Error type 是實現了 error 接口的自定義類型。例如 MyError 類型記錄了文件和行號以及展示發生了什么。

因為 MyError 是一個type,調用者可以使用斷言轉換成這個類型,來獲取更多的上下文信息。

如下代碼所示:

package main

import (
  "fmt"
)

type MyError struct {
    Msg string
    File string
    Line int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s:%d:%s",e.File,e.Line,e.Msg)
}

func test() error {
    return &MyError{"Something happened", "server.go", 42}
}

func main() {
    err := test()

    switch err := err.(type) {
        case nil:
            // call succeeded,nothing to do
        case *MyError:
                fmt.Println("error occurred on line:", err.Line)
        default:
                // unkonwn error
    }
}

調用者要使用類型斷言和類型 switch,就要讓自定義的 error 變為public。這種模型會導致和調用者產生強耦合,從而導致API變得脆弱。

結論是:

盡量避免使用 error types,雖然錯誤類型比 sentinel errors 更好,因為它們可以捕獲關於出錯的更多上下文,但是 error type 共享 error values 需要相同的問題。

所以盡量避免錯誤類型,或者至少避免將它們成為公共API的一部分。

Opaque errors

不透明錯誤處理,雖然您知道了發生了錯誤,但您沒有能力看到錯誤的內部。作為調用者,關於操作的結果,您所知道的就是它起作用了,或者沒有起作用(成功還是失敗)。

import "github.com/quux/bar"

func fn() error {
     x,err := bar.Foo()
    
     if err != nil {
            return err
      } 
      // use x    
}

Assert errors for behaviour,not ype

二分錯誤處理方法是不夠的。調用方需要調查錯誤的性質,以確定重試該操作是否合理。在這種情況下,我們可以斷言錯誤實現了特定的行為,而不是斷言錯誤是特定的類型或值。

package net

type Error interface {
     error
     Timeout() bool // Is the error a timeout?
     Temporary() bool // Is the error temporary? 
}

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
     te,ok := err.(temporary)
     return ok && te.Temporary()
}

if nerr,ok := err.(net.Error); ok && nerr.Temporary() {
     time.Sleep(1e9)
     continue
}

if err != nil {
  log.Fatal(err)
}

關鍵邏輯是可以在不導入定義錯誤的包或實際上不了解err的底層類型的情況下實現 --- 我們只對它的行為感興趣。

Handling Error

Indented flow is for errors

無錯誤的正常錯誤代碼,將成為一條直線,而不是縮進的代碼.fail fast

第一種會更好

f,err := os.Open(path)
if err != nil {
  // handle error
}
// do stuff
f,errr := os.Open(path)
if err == nil {
    // do stuff
}
// handle error

Eliminate error handling by eliminating errors

如果錯誤類型相同直接返回

func AuthenticateRequest(r *Request) error {
     err := authenticate(r.User)
     if err != nil {
         return err
     }
    return nil
}

上面這種可以直接返回

func AuthenticateRequest(r *Request) error {
     return authenticate(r.User)
}

單獨一個方法去判斷是否有錯誤產生

func CountLines(r io.Reader) (int, error) {
     var (
            br = bufio.NewReader(r)
            line int
            err error
      )  

      for {
            _, err = br.ReadString('\n')
           lines++
            if err != nil {
                break
           }
       }

     if err != io.EOF {
          return 0,err
      }

      return line,nil
}

使用 sc.Scan() 方法,最后判斷 sc.Err()

func CountLines(r io.Reader) (int,error) {
      sc := bufio.NewScanner(r)
     
      line := 0
     
      for sc.Scan()  {
          lines++
      }

      return lines,sc.Err()
}

定義結構體,方法中去判斷是否有錯誤

type Header struct {
    Key,Value string
}

type Status struct {
    Code int
    Reason string
}

func WriteResponse(w io.Writer,st Status,headers []Header, body io.Reader) error {
      _,err := fmt.Fprint(w, "HTTP/1.1 %d %s\r\n", st.Code,st.Reason)
     if err != nil {
          return err
     }

     for _, h := range headers {
          _, err := fmt.Fprint(w, "%s: %s\r\n",h.Key,h.Value)
          if err != nil {
                  return err
           }
     }

     if _,err := fmt.Fprint(w, "\r\n"); err != nil {
          return err
     }
     
     _,err = io.Copy(w, body)
     return err
}

添加結構體定義,在Write 里面處理 err

type errWriter struct {
     io.Writer
     err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
    if e.err != nil {
           return 0,e.err
     }
    
     var n int
      n,e.err = e.Writer.Write(buf)
      return n,nil
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        ew := &errWriter{Writer: w}
  
        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code,st.Reason)
     
        for _,h := range headers {
              fmt.Fprintf(ew, "%s: %s\r\n", h.Key,h.Value)
        }

        fmt.Fprint(ew, "\r\n")
        io.Copy(ew, body)

        return ew.err
}

Wrap errors

func AuthenticateRequest(r *Request) error {
    return authenticate(r.User)
}

如果 authenticate 返回錯誤,則 AuthenticateRequest 會將錯誤返回給調用方,調用者可能也會這樣做,依次類推。在程序的頂部,程序的主體將把錯誤打印到屏幕或日志文件中,打印出來的只是: 沒有這樣的文件或目錄。

修改一下

func AuthenticateRequest(r *Request) error {
     err := authenticate(r.User)
     if err != nil {
            return fmt.Errorf("authenticate failed: %v", err)
     }
     return nil
}

沒有生成錯誤的 file:line 信息,沒有導致錯誤的調用堆棧的堆棧跟蹤。

這種模式與 sentinel errors 或 type assertions 的使用不兼容,因為將錯誤值轉換為字符串,將其與另一個字符串合並,然后將其轉換回 fmt.ErrorF 破壞了 原始錯誤,導致等值判定失敗。

只處理一次錯誤,不要到處拋

you should only handle errors once. Handling an error means inspecting the error value, and making a single decision.

我們經常發現類似的代碼,在錯誤處理中,帶了兩個任務:記錄日志並且再次返回錯誤。

func WriteAll(w io.Writer,buf []byte) error {
     _,err := w.Write(buf)
     
     if err != nil {
          log.Println("unable to write:", err)  // annotated error goes to log file
          return err                                  // unannotated error returned to caller
     }

    return nil
}

日志是否記錄與錯誤處理的關系

日志記錄與錯誤無關且對調試沒有幫助的信息應被視為噪音,應予以質疑。記錄的原因是因為某些東西失敗了,而日志包含了答案。

The error has been logged.

錯誤要被日志記錄。

The application is back to 100% integrity.

應用程序處理錯誤,保證100%完整性。

The current error is not reported any longer.

之后不再報告當前錯誤。

如果吞掉錯誤,必須對value 負起責任

  1. 返回默認值

  2. 返回降級數據信息

Wrap errors

通過使用 pkg/errors 包,您可以想錯誤值添加上下文,這種方式既可以由人也可以由機器檢查

Wrap error 原理

Wrap -> err 熟悉->
WithMessage {
cause: 上層 err
}
-> withStack {
cause: 上層 withMessage
}

使用 errors.Cause 獲取 root error,再進行和 sentinel error 判定

總結:

Packages that are reusable across many projucts only return root error values

選擇 wrap error 是只有 applications 可以選擇應用的策略。具有最高可重用性的包只能返回根錯誤值。此機制與Go標准庫中使用的相同(kit 庫的sql.ErrNoRows).

If the error is not going to be handled,wrap and return up the call stack.

如果函數/方法不打算處理錯誤,那么用足夠的上下文 wrap errors 並將其返回到調用堆棧中。例如,額外的上下文可以是使用的輸入參數或失敗的查詢語句。記錄的上下文足夠多還是太多的一個好方法是檢查日志並驗證它們在開發期間是否為您工作。

Once an error is handled,it is not allowed to be passed up the call stack any longer.

一旦確定函數/方法將處理錯誤,錯誤就不再是錯誤。如果函數/方法仍然需要發出返回,則它不能返回錯誤值。它應該只返回零(比如降級處理中,你返回了降級數據,然后需要 return nil).

參考文章

Errors are values : https://blog.golang.org/errors-are-values

Error handling and Go: https://blog.golang.org/error-handling-and-go

Go錯誤處理最佳實踐: https://lailin.xyz/post/go-training-03.html

毛劍老師: 極客時間 Go 進階訓練營


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM