error定義
數據結構
go語言error是一普通的值,實現方式為簡單一個接口。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
創建error
使用errors.New()
// 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結構體 實現了error接口的Error()方法
使用fmt.Errorf()創建
創建方式為把字符串拼接起來,然后調用errors.New().
基礎庫中的自定義的error
bufio中的錯誤:
ErrTooLong = errors.New("bufio.Scanner: token too long")
ErrNegativeAdvance = errors.New("bufio.Scanner: SplitFunc returns negative advance count")
ErrAdvanceTooFar = errors.New("bufio.Scanner: SplitFunc returns advance count beyond input")
ErrBadReadCount = errors.New("bufio.Scanner: Read returned impossible count")
error的比較
package main
import (
"errors"
"fmt"
)
type errorString struct {
s string
}
func new(s string) error {
return &errorString{s: s}
}
func (e *errorString) Error() string {
return e.s
}
func main() {
error1 := errors.New("test")
error2 := new("test")
fmt.Println(error1 == error2) // false
}
// 比較結構體
package main
import (
"fmt"
)
type errorString struct {
s string
}
func new(s string) error {
return &errorString{s: s}
}
func (e *errorString) Error() string {
return e.s
}
func main() {
error1 := new("test")
fmt.Println(error1 == new("test")) // false
}
package main
import (
"fmt"
)
type errorString struct {
s string
}
func new(s string) error {
return errorString{s: s}
}
func (e errorString) Error() string {
return e.s
}
func main() {
error1 := new("test")
fmt.Println(error1 == new("test")) // true
}
error對比為對比對比實現interface的結構體類型 和結構體本身
Error or Exception
處理錯誤的演進
- C
- 單返回值,一般通過傳遞指針作為入參,返回值為 int 表示成功還是失敗。
- C++
- 引入了 exception,但是無法知道被調用方會拋出什么異常。
- Java
- 引入了 checked exception,方法的所有者必須申明,調用者必須處理。在啟動時拋出大量的異常是司空見慣的事情,並在它們的調用堆棧中盡職地記錄下來。Java 異常不再是異常,而是變得司空見慣了。它們從良性到災難性都有使用,異常的嚴重性由函數的調用者來區分
- go
-
Go 的處理異常邏輯是不引入 exception,支持多參數返回,所以你很容易的在函數簽名中帶上實現了 error interface 的對象,交由調用者來判定。
-
如果一個函數返回了 value, error,你不能對這個 value 做任何假設,必須先判定 error。唯一可以忽略 error 的是,如果你連 value 也不關心。
-
Go 中有 panic 的機制,如果你認為和其他語言的 exception 一樣,那你就錯了。當我們拋出異常的時候,相當於你把 exception 扔給了調用者來處理。比如,你在 C++ 中,把 string 轉為 int,如果轉換失敗,會拋出異常。或者在 java 中轉換 string 為 date 失敗時,會拋出異常。
-
Go panic 意味着 fatal error(就是掛了)。不能假設調用者來解決 panic,意味着代碼不能繼續運行。
-
使用多個返回值和一個簡單的約定,Go 解決了讓程序員知道什么時候出了問題,並為真正的異常情況保留了 panic。
代碼對比
package main
import "fmt"
func Positive(x int) bool {
return x >= 0
}
func Check(x int) {
if Positive(x) {
fmt.Println("正數")
} else {
fmt.Println("負數")
}
}
func main() {
Check(-1) // 負數
Check(0) // 正數 bug
Check(1) // 正數
}
package main
import "fmt"
func Positive(x int) (bool, bool) {
if x == 0 {
return false, false
}
return x >= 0, true
}
func Check(x int) {
t, ok := Positive(x)
if !ok {
fmt.Println("零")
return
}
if t {
fmt.Println("正數")
} else {
fmt.Println("負數")
}
}
func main() {
Check(-1) // 負數
Check(0) // 零
Check(1) // 正數
}
package main
import (
"errors"
"fmt"
)
func Positive(x int) (bool, error) {
if x == 0 {
return false, errors.New("為零")
}
return x >= 0, nil
}
func Check(x int) {
t, err := Positive(x)
if err != nil {
fmt.Println(err)
return
}
if t {
fmt.Println("正數")
} else {
fmt.Println("負數")
}
}
func main() {
Check(-1) // 負數
Check(0) // 為零
Check(1) // 正數
}
error使用
對於真正意外的情況,那些表示不可恢復的程序錯誤,例如索引越界、不可恢復的環境問題、棧溢出,我們才使用 panic。對於其他的錯誤情況,我們應該是期望使用 error 來進行判定。
-
簡單。
-
考慮失敗,而不是成功。
-
沒有隱藏的控制流。
-
完全交給你來控制 error。
-
Error are values。
Sentinel Error
預定義的特定錯誤,我們叫為 sentinel error,這個名字來源於計算機編程中使用一個特定值來表示不可能進行進一步處理的做法。所以對於 Go,我們使用特定的值來表示錯誤。
if err == ErrSomething { … }
類似的 io.EOF,更底層的 syscall.ENOENT。
使用 sentinel 值是最不靈活的錯誤處理策略,因為調用方必須使用 == 將結果與預先聲明的值進行比較。當您想要提供更多的上下文時,這就出現了一個問題,因為返回一個不同的錯誤將破壞相等性檢查。
甚至是一些有意義的 fmt.Errorf 攜帶一些上下文,也會破壞調用者的 == ,調用者將被迫查看 error.Error() 方法的輸出,以查看它是否與特定的字符串匹配。
- 不依賴檢查 error.Error 的輸出。
不應該依賴檢測 error.Error 的輸出,Error 方法存在於 error 接口主要用於方便程序員使用,但不是程序(編寫測試可能會依賴這個返回)。這個輸出的字符串用於記錄日志、輸出到 stdout 等。
- Sentinel errors 成為你 API 公共部分。
如果您的公共函數或方法返回一個特定值的錯誤,那么該值必須是公共的,當然要有文檔記錄,這會增加 API 的表面積。
如果 API 定義了一個返回特定錯誤的 interface,則該接口的所有實現都將被限制為僅返回該錯誤,即使它們可以提供更具描述性的錯誤。
比如 io.Reader。像 io.Copy 這類函數需要 reader 的實現者比如返回 io.EOF 來告訴調用者沒有更多數據了,但這又不是錯誤。
- Sentinel errors 在兩個包之間創建了依賴。
sentinel errors 最糟糕的問題是它們在兩個包之間創建了源代碼依賴關系。例如,檢查錯誤是否等於 io.EOF,您的代碼必須導入 io 包。這個特定的例子聽起來並不那么糟糕,因為它非常常見,但是想象一下,當項目中的許多包導出錯誤值時,存在耦合,項目中的其他包必須導入這些錯誤值才能檢查特定的錯誤條件(in the form of an import loop)。
- 結論: 盡可能避免 sentinel errors。
我的建議是避免在編寫的代碼中使用 sentinel errors。在標准庫中有一些使用它們的情況,但這不是一個您應該模仿的模式。
錯誤類型
Error type 是實現了 error 接口的自定義類型。例如 MyError 類型記錄了文件和行號以展示發生了什么。
type Myerror struct {
line int
file string
s string
}
func (e *Myerror) Error() string {
return e.s
}
func new(file string, line int, s string) error {
return &Myerror{line: line, file: file, s: s}
}
因為 MyError 是一個 type,調用者可以使用斷言轉換成這個類型,來獲取更多的上下文信息。
err := new("main.go", 23, "test error")
switch err := err.(type) {
case nil:
fmt.Println("err is nil")
case *Myerror:
fmt.Println("type is *Myerror err line :", err.line)
default:
fmt.Println("None of them")
}
// 結果:type is *Myerror err line : 23
與錯誤值相比,錯誤類型的一大改進是它們能夠包裝底層錯誤以提供更多上下文。
一個不錯的例子就是 os.PathError 他提供了底層執行了什么操作、那個路徑出了什么問題。
調用者要使用類型斷言和類型 switch,就要讓自定義的 error 變為 public。這種模型會導致和調用者產生強耦合,從而導致 API 變得脆弱。
結論是盡量避免使用 error types,雖然錯誤類型比 sentinel errors 更好,因為它們可以捕獲關於出錯的更多上下文,但是 error types 共享 error values 許多相同的問題。
因此,我的建議是避免錯誤類型,或者至少避免將它們作為公共 API 的一部分。
非透明的error
在我看來,這是最靈活的錯誤處理策略,因為它要求代碼和調用者之間的耦合最少。
我將這種風格稱為不透明錯誤處理,因為雖然您知道發生了錯誤,但您沒有能力看到錯誤的內部。作為調用者,關於操作的結果,您所知道的就是它起作用了,或者沒有起作用(成功還是失敗)。
這就是不透明錯誤處理的全部功能–只需返回錯誤而不假設其內容
package main
import "os"
func test() error {
f, err := os.Open("filename.txt")
if err != nil {
return err
}
// use f
}
為行為而不是類型斷言錯誤 在少數情況下,這種二分錯誤處理方法是不夠的。例如,與進程外的世界進行交互(如網絡活動),需要調用方調查錯誤的性質,以確定重試該操作是否合理。在這種情況下,我們可以斷言錯誤實現了特定的行為,而不是斷言錯誤是特定的類型或值。考慮這個例子:
// 封裝內部
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
// net包的error
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
// 錯誤處理
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
// 處理
return
}
if err != nil {
}
Handling Error
無錯誤的正常流程代碼,將成為一條直線,而不是縮進的代碼。
f,err := os.Open("file")
if err != nil {
// 處理錯誤
return
}
// 邏輯
f,err = os.Open("file2")
if err != nil {
// 處理錯誤
return
}
// 邏輯
通過消除錯誤消除錯誤處理
// 改進前
func AutoRusquest() (err error) {
err = Anto()
if err != nil {
return
}
return
}
// 改進后
func AutoRusquest() (err error) {
return Anto()
}
func main() {
err := AutoRusquest()
if err != nil {
// log
}
}
// 統計行數
func Countlines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
if err != io.EOF {
return 0, err
}
return lines, nil
}
// 改進后
func Countlines(r io.Reader) (int, error) {
sr := bufio.NewScanner(r)
lines := 0
for sr.Scan() {
lines ++
}
return lines,sr.Err()
}
Wrap errors
傳統error的問題
還記得之前我們 auth 的代碼吧,如果 Auto 返回錯誤,則 Aut0Request 會將錯誤返回給調用方,調用者可能也會這樣做,依此類推。在程序的頂部,程序的主體將把錯誤打印到屏幕或日志文件中,打印出來的只是:沒有這樣的文件或目錄。
沒有生成錯誤的 file:line 信息。沒有導致錯誤的調用堆棧的堆棧跟蹤。這段代碼的作者將被迫進行長時間的代碼分割,以發現是哪個代碼路徑觸發了文件未找到錯誤。
func AutoRusquest() (err error) {
err = Anto()
if err != nil {
err = fmt.Errorf("auto failed:%v",err)
return
}
return
}
但是正如我們前面看到的,這種模式與 sentinel errors 或 type assertions 的使用不兼容,因為將錯誤值轉換為字符串,將其與另一個字符串合並,然后將其轉換回 fmt.Errorf 破壞了原始錯誤,導致等值判定失敗。
你應該只處理一次錯誤。處理錯誤意味着檢查錯誤值,並做出單個決策。
func WriteAll(w io.Writer, buf []byte) {
w.Write(buf)
}
我們經常發現類似的代碼,在錯誤處理中,帶了兩個任務: 記錄日志並且再次返回錯誤。
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Panicln("write buf failed:", err)
return err
}
return nil
}
在這個例子中,如果在 w.Write 過程中發生了一個錯誤,那么一行代碼將被寫入日志文件中,記錄錯誤發生的文件和行,並且錯誤也會返回給調用者,調用者可能會記錄並返回它,一直返回到程序的頂部。
func WriteConfig(w *io.Writer,config *Config) {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %V", err)
return err
}
if err := Writeall(w, buf); err != nil {
log.Printf("could not write config: %v", err)
return err
}
}
func main() {
err := Writeconfig(f, &conf)
fmt.Println(err)
}
/*
unable to write: io.EOF
could not write config: io.EOF
*/
Go 中的錯誤處理契約規定,在出現錯誤的情況下,不能對其他返回值的內容做出任何假設。由於 JSON 序列化失敗,buf 的內容是未知的,可能它不包含任何內容,但更糟糕的是,它可能包含一個半寫的 JSON 片段。
由於程序員在檢查並記錄錯誤后忘記 return,損壞的緩沖區將被傳遞給 WriteAll,這可能會成功,因此配置文件將被錯誤地寫入。但是,該函數返回的結果是正確的。
棧處理錯誤
日志記錄與錯誤無關且對調試沒有幫助的信息應被視為噪音,應予以質疑。記錄的原因是因為某些東西失敗了,而日志包含了答案。
-
錯誤要被日志記錄。
-
應用程序處理錯誤,保證100%完整性。
-
之后不再報告當前錯誤。
包:github.com/pkg/errors
func main() {
_, err := Readconfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func Readfile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
func Readconfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := Readfile(filepath.Join(home, "settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}
/*
could not read config: open failed: open /Users/zhaohaiyu/settings.xml: no such file or directory
exit status 1
*/
func main() {
_, err := Readconfig()
if err != nil {
fmt.Printf("original error: %T -> %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace: \n%+v\n", err)
os.Exit(1)
}
}
/*
original error: *os.PathError -> open /Users/zhaohaiyu/settings.xml: no such file or directory
stack trace:
open /Users/zhaohaiyu/settings.xml: no such file or directory
open failed
main.Readfile
/Users/zhaohaiyu/code/test/main.go:35
main.Readconfig
/Users/zhaohaiyu/code/test/main.go:51
main.main
/Users/zhaohaiyu/code/test/main.go:22
runtime.main
/usr/local/Cellar/go/1.15.3/libexec/src/runtime/proc.go:204
runtime.goexit
/usr/local/Cellar/go/1.15.3/libexec/src/runtime/asm_amd64.s:1374
could not read config
exit status 1
*/
通過使用 pkg/errors 包,您可以向錯誤值添加上下文,這種方式既可以由人也可以由機器檢查。
errors.Wrap(err, "read failed")
wrap errors使用
- 在你的應用代碼中,使用 errors.New 或者 errros.Errorf 返回錯誤。
func parseargs(args []string) error {
if len(args) < 3 {
return errors.Errorf("not enough arguments, expected at Least")
}
// ...
return nil
}
- 如果調用其他的函數,通常簡單的直接返回。
if err != nil {
return err
}
- 如果和其他庫進行協作,考慮使用 errors.Wrap 或者 errors.Wrapf 保存堆棧信息。同樣適用於和標准庫協作的時候。
f, err := os.Open(file)
if err != nil {
return errors.Wrapf(err, "open %s failed",file)
}
-
直接返回錯誤,而不是每個錯誤產生的地方到處打日志。
-
在程序的頂部或者是工作的 goroutine 頂部(請求入口),使用 %+v 把堆棧詳情記錄。
func main() {
err := app.Run()
if err != nil {
fmt.Printf("FATAL:%+v\n", err)
os.Exit(1)
}
}
- 使用 errors.Cause 獲取 root error,再進行和 sentinel error 判定。
總結:
-
Packages that are reusable across many projects 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)。)
Go1.13 error
1
函數在調用棧中添加信息向上傳遞錯誤,例如對錯誤發生時發生的情況的簡要描述。
if err != nil {
return fmt.Errorf("decompress %v:%v", name, err)
}
使用創建新錯誤 fmt.Errorf 丟棄原始錯誤中除文本外的所有內容。正如我們在上面的QueryError 中看到的那樣,我們有時可能需要定義一個包含底層錯誤的新錯誤類型,並將其保存以供代碼檢查。這里是 QueryError:
type QueryError struct {
Query string
Err error
}
程序可以查看 QueryError \p值以根據底層錯誤做出決策。
if e, ok := err.(*Queryerror); ok && e.Err == ErrPermission {
//query failed because of a permission problem
}
go1.13為 errors 和 fmt 標准庫包引入了新特性,以簡化處理包含其他錯誤的錯誤。其中最重要的是: 包含另一個錯誤的 error 可以實現返回底層錯誤的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我們說 e1 包裝 e2,您可以展開 e1 以獲得 e2。
按照此約定,我們可以為上面的 QueryError 類型指定一個 Unwrap 方法,該方法返回其包含的錯誤:
func (e *Queryerror) Unwrap() error { return e.Err }
go1.13 errors 包包含兩個用於檢查錯誤的新函數:Is 和 As。
// Similar to:
// if err = Errnotfound {...}
if errors.Is(err, Errnotfound) {
// something wasnt found
}
// Similar to
// if e, ok := err.(*Queryerror); ok {...}
var e *Queryerror
// Note: *Queryerror is the type of the error
if errorsAs(err, &e) {
// err is a *Queryerror, and e is set to the errors value
}
Wrapping errors with %w
如前所述,使用 fmt.Errorf 向錯誤添加附加信息。
if err != nil {
return fmt.Errorf("decompress %v:%v", name, err)
}
在 Go 1.13中 fmt.Errorf 支持新的 %w 謂詞。
if err != nil {
return fmt.Errorf("decompress %v:%w", name, err)
}
用 %w 包裝錯誤可用於 errors.Is 以及 errors.As
err := fmt.Errorf("access denied: % W", Errpermission)
if errors.Is(err, Errpermission) {
// ...
}
Go2介紹
https://go.googlesource.com/proposal/+/master/design/29934-error-values.md
