前言:
此文是觀看毛劍老師的《GO進階訓練營》中的《異常處理》的個人理解和總結,如果有不對的地方,還望指正,謝謝?。
error和exception
exception
無法區分普通異常和致命異常,始終需要使用try...catch
命令去捕捉異常,在自己編碼中經常會去考慮是否對其異常捕獲,甚至有時會對異常catch
多次。
而go
中的error
利用多返回值,使用普通error
和panic
分別代表了普通異常和致命異常,更加明確了對異常的處理。
一、什麼是error
go
中的error
實際上是實現了error
介面的普通型別,這樣可以很容易實現自定義的error
。
// https://golang.org/src/builtin/builtin.go
type error interface {
Error() string
}
官方標準庫中的errors
:
// https://golang.org/src/errors/errors.go
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
其中New
方法返回errorString
記憶體地址是為了避免在使用相同errors
包時,不同的錯誤因為傳入的文字資訊相同而相等。
package main
import "fmt"
type myError struct {
s string
}
func (m myError) Error() string {
return m.s
}
func New(text string) error {
return myError{text}
}
func main() {
aErr := New("error")
bErr := New("error")
if aErr == bErr {
fmt.Println("aErr == bErr") // aErr == bErr
}
}
上述程式碼會認為分別New
出來的error
時相等的,這樣明顯是不正確的,當修改返回的error
是記憶體地址時,即使相同文字也不會相等。
二、error和panic使用場景
error
一般用於可遇見的普通異常,比如引數錯誤、查詢錯誤等異常;而panic
一般用於強依賴的第三方、配置資訊發生錯誤,比如db無法連線、配置引數不存在等異常,但是使用方式需要根據實際程式碼邏輯進行判斷。
三、error的幾種使用方式
1、Sentinal Error
Sentinal Error
是預先在包中定義的特定錯誤,比如io
包中的EOF
:
// https://golang.org/src/io/io.go
var EOF = errors.New("EOF")
io.EOF
是用於在檔案讀取時沒有內容返回的錯誤,所以在外部接收到error
時需要判斷是否等於io.EOF
:
if err != nil {
if err == io.EOF {
// do something
}
}
使用Sentinal Error
有以下不足:
無法攜帶多餘的上下文資訊,如果使用
fmt.Errorf
新增自定義資訊,返回的錯誤無法再與Sentinal Error
進行比較。使包與包之間產生耦合,假如存在
Sentinal Error
的包是專案的基礎包,引入這個基礎的其他包都必須匯入這些錯誤值,增加了耦合性的同時基礎包也無法再對這些錯誤值進行相關修改。
所以Sentinal Error
儘量避免在基礎包(或者很多包引用的包)中使用。
2、Error Type
Error Type
是實現了error
介面的自定義型別,可以保留底層的錯誤同時增加上下文資訊。
例如myError
型別記錄了檔案和行號的上下文:
package main
import (
"fmt"
)
type myError struct {
s string
file string
line int
}
func (m *myError) Error() string {
return fmt.Sprintf("%s-%d: %s", m.file, m.line, m.s)
}
func New(text, file string, line int) error {
return &myError{text, file, line}
}
func main() {
err := do()
switch err := err.(type) {
case nil:
// no error
case *myError:
fmt.Printf("error occurred: %v\n", err.Error())
default:
// unknown error
}
}
func do() error {
return New("something error", "main.go", 27)
}
Error Type
可以使用斷言轉換成這個型別,來獲取上下文資訊,但是Error Type
仍然存在與其他包產生耦合的問題。
3、Opaque errors(非透明錯誤處理)
Opaque erros
只關心返回的錯誤而不是錯誤內容。
func fn() error {
i, err := do()
if err != nil {
return err
}
// do something
}
Opaque errors
的好處減少了程式碼間的耦合性,但是在很多時候仍然需要確認返回錯誤的性質,以便進一步處理,比如網路請求是超時還是返回錯誤,此時推薦使用斷言錯誤的行為,而不是斷言錯誤的型別。
例如:
定義timeout
介面,IsTimeout
方法判斷錯誤的行為。
type timeout interface {
Timeout() bool
}
func IsTimeout(err error) bool {
t, ok := err.(timeout)
return ok && t.Timeout()
}
當相關error
實現Timeout
方法後即可判斷其存在超時行為。
if IsTimeout(err) {
// time out
}
這裡關鍵,可以在不到入定義錯誤的包或者不瞭解底層型別時判斷錯誤的性質。
4、Wrap Error
Wrap Error
可以將最底層的錯誤儲存的同時新增更多上下文資訊,需要判斷錯誤性質時可以獲取底層錯誤。
Wrap Error
目前有兩種方式,一種是標準庫的errors
包,另外一種就是第三方包github.com/pkg/errors
(下稱pkg/errors
),在目前go 1.x
版本下,pkg/errors
相比於errors
包可以記錄堆疊資訊,方便除錯。
package main
import (
"errors"
"fmt"
pkError "github.com/pkg/errors"
)
func main() {
err := service()
fmt.Printf("casue err: %+v\n", pkError.Cause(err))
fmt.Printf("err: %+v", err)
}
func service() error {
err := biz()
return pkError.WithMessage(err, "this is service")
}
func biz() error {
err := dao()
return pkError.WithMessage(err, "this is biz")
}
func dao() error {
err := errors.New("dao err")
return pkError.Wrap(err, "this is dao")
}
// 列印資訊
// casue err: dao err
// err: dao err
// this is dao
// main.dao
// .../main.go:26 main.biz
// .../main.go:20 main.service
// .../main.go:15 main.main
// .../main.go:10 runtime.main
// /usr/local/go/src/runtime/proc.go:225 runtime.goexit
// /usr/local/go/src/runtime/asm_amd64.s:1371
// this is biz
// this is service
這裡假設dao
層獲取對資料的操作錯誤後,使用pkg/errors
將錯誤Wrap
後返回,biz
層獲取到dao
層錯誤使用WithMessage
增加相關上下文,最終傳遞上層可列印出上下文和堆疊資訊,並且可以使用Cause
(或者Unwrap
)獲取最底層錯誤。
當需要比較錯誤時,可使用Is
和As
方法,如果錯誤已經被Wrap
,它們會獲取最底層錯誤進行比較。
func Is(err, target error) bool
func As(err, target interface{}) bool
As
相比Is
,As
會在err
的底層錯誤與target
相等時,將err
賦值給target
。
Wrap Error
小結:Wrap Error
可以保留Sentinal Error
特性的同時攜帶上下文資訊,一定程度上也相容了Error Type
特性,在日常編碼中推薦使用這種方式。
注意事項:
1. 如果函式返回了value,error
,必須判定error
,否則返回的value
不可用,除非你連value
也不關心
2. error
應該只被處理一次(列印日誌也算是對error
的一種處理),要麼降級處理,要麼向上層返回error
。
3. 當與第三方庫或者基礎庫互動時,可以將第一次錯誤Wrap
(只能Wrap
一次,否則將重複列印堆疊資訊)向上返回,最終在頂層列印日誌,可以避免日誌的重複列印及除錯方便。
4.使用哪種error
的處理方式,最終由使用場景來決定。
本作品採用《CC 協議》,轉載必須註明作者和本文連結