Golang Error

問題

https://go.dev/play/p/tul2xxXCr1K

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("start main")
    go func() {
        fmt.Println("Hello")
        panic("PANIC!")
    }()
    time.Sleep(3 * time.Second)
}

發出panic後 整個程式就掛掉了

訊息:

start main
Hello
panic: PANIC!

goroutine 18 [running]:
main.main.func1()
    /tmp/sandbox1853272060/prog.go:12 +0x65
created by main.main
    /tmp/sandbox1853272060/prog.go:10 +0x5e

Program exited.
改良:
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("start main")
    Go(func() {
        fmt.Println("Hello")
        panic("PANIC!")
    })
    time.Sleep(3 * time.Second)
}

func Go(x func()) {
    go func() {
        defer func() {
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()

        x()
    }()
}

Error

對於真正意外的情況,那些表示不可恢復的程序錯誤, 例如索引越界、不可恢復的環境問題、棧溢出,我們才使用 panic 。 對於其他的錯誤情況,我們應該是期望使用 error 來進行判定。

you only need to check the error value if you care about the result. -- Dave This blog post from Microsoft’s engineering blog in 2005 still holds true today, namely: My point isn’t that exceptions are bad. My point is that exceptions are too hard and I’m not smart enough to handle them.

  • 簡單
  • 考慮失敗,而不是成功(Plan for failure, not success)
  • 沒有隱藏的控制流
  • 完全交給你來控制 error
  • Error are values

Error types

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

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}
}

與錯誤值相比,錯誤類型的一大改進是它們能夠包裝底層錯誤以提供更多上下文。 一個不錯的例子就是 os.PathError 他提供了底層執行了什麼操作、那個路徑出了什麼問題。

調用者要使用類型斷言和類型 switch,就要讓自定義的 error 變為 public。 這種模型會導致和調用者產生強耦合,從而導致 API 變得脆弱。 結論是盡量避免使用 error types,雖然錯誤類型比 sentinel errors 更好, 因為它們可以捕獲關於出錯的更多上下文, 但是 error types 共享 error values 許多相同的問題。 因此,我的建議是避免錯誤類型,或者至少避免將它們作為公共 API 的一部分。

Opaque errors (建議)

Opaque adjective. /oʊˈpeɪk/ : 不透明的;不透光的

在我看來,這是最靈活的錯誤處理策略,因為它要求代碼和調用者之間的耦合最少。 我將這種風格稱為不透明錯誤處理,因為雖然您知道發生了錯誤,但您沒有能力看到錯誤的內部。作為調用者,關於操作的結果,您所知道的就是它起作用了,或者沒有起作用(成功還是失敗)。 這就是不透明錯誤處理的全部功能–只需返回錯誤而不假設其內容。

import "github.com/xxxx/bar"

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

Assert errors for behaviour, not type

在少數情況下,這種二分錯誤處理方法是不夠的。 例如,與進程外的世界進行交互(如網絡活動),需要調用方調查錯誤的性質, 以確定重試該操作是否合理。 在這種情況下,我們可以斷言錯誤實現了特定的行為,而不是斷言錯誤是特定的類型或值。 考慮這個例子:

src/net/net.go

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

    // Deprecated: Temporary errors are not well-defined.
    // Most "temporary" errors are timeouts, and the few exceptions are surprising.
    // Do not use this method.
    Temporary() bool
}

type temporary interface {
    Temporary() bool
}

func (e *OpError) Temporary() bool {
    // Treat ECONNRESET and ECONNABORTED as temporary errors when
    // they come from calling accept. See issue 6163.
    if e.Op == "accept" && isConnError(e.Err) {
        return true
    }

    if ne, ok := e.Err.(*os.SyscallError); ok {
        t, ok := ne.Err.(temporary)
        return ok && t.Temporary()
    }
    t, ok := e.Err.(temporary)
    return ok && t.Temporary()
}

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

Error Handle

Eliminate error handling by eliminating errors

統計 io.Reader 讀取內容的行數

改進版本:

Wrap Erros

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

在這個例子中,如果在 w.Write 過程中發生了一個錯誤,那麼一行代碼將被寫入日誌文件中, 記錄錯誤發生的文件和行,並且錯誤也會返回給調用者, 調用者可能會記錄並返回它,一直返回到程序的頂部。

日誌記錄與錯誤無關且對調試沒有幫助的信息應被視為噪音,應予以質疑。 記錄的原因是因為某些東西失敗了,而日誌包含了答案。 The error has been logged. The application is back to 100% integrity. The current error is not reported any longer.

  • 錯誤要被日誌記錄。
  • 應用程序處理錯誤,保證100%完整性。
  • 之後不再報告當前錯誤。

github.com/pkg/errors

小筆記

  1. 在你的應用代碼中,使用 errors.New 或者 errros.Errorf 返回錯誤
        func parseArgs(args []string) error {
         if len(args) < 3 {
             return errors.Errorf("not enough arguments, expected at least")
         }
         // ...
        }
    
  2. 如果調用其他的函數,通常簡單的直接返回。

    if err != nil {
     return err
    }
    
  3. 如果和其他庫(Github / 自己的基礎庫 / 標準庫...)進行協作,考慮使用 errors.Wrap 或者 errors.Wrapf 保存堆棧信息。同樣適用於和標準庫協作的時候。

    f, err := os.Open(path)
    if err != nil {
     return errors.Wrapf(err, "failed to open %q", path)
    }
    
  4. 直接返回錯誤,而不是每個錯誤產生的地方到處打日誌。

  5. 在程序的頂部或者是工作的 goroutine 頂部(請求入口),使用 %+v 把堆棧詳情記錄

    func main(){
     err := app.Run()
     if err != nil {
         fmt.Printf("FATAL: %+v\n",err)
         os.Exit()
     }
    }
    
  6. 使用 errors.Cause 獲取 root error,再進行和 sentinel error 判定。

總結

  • Packages that are reusable across many projects only return root error values. (譬如基礎庫, 不應該 wrap error, 業務的才需要 wrap) 選擇 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)。
© Kimi Tsai all right reserved.            Updated : 2023-07-12 09:04:53

results matching ""

    No results matching ""

    results matching ""

      No results matching ""