新聞中心
大家好,這里是每周都在陪你進步的網(wǎng)管。

創(chuàng)新互聯(lián)成都企業(yè)網(wǎng)站建設(shè)服務(wù),提供網(wǎng)站制作、成都網(wǎng)站建設(shè)網(wǎng)站開發(fā),網(wǎng)站定制,建網(wǎng)站,網(wǎng)站搭建,網(wǎng)站設(shè)計,響應(yīng)式網(wǎng)站建設(shè),網(wǎng)頁設(shè)計師打造企業(yè)風(fēng)格網(wǎng)站,提供周到的售前咨詢和貼心的售后服務(wù)。歡迎咨詢做網(wǎng)站需要多少錢:028-86922220
之前寫過幾篇關(guān)于 Go 錯誤處理的文章,發(fā)現(xiàn)文章里不少知識點都有點落伍了,比如Go在1.13后對錯誤處理增加了一些支持,最大的變化就是支持了錯誤包裝(Error Wrapping),以前想要在調(diào)用鏈路的函數(shù)里包裝錯誤都是用"github.com/pkg/errors"這個庫。
Go 在2019年發(fā)布的Go1.13版本也采納了錯誤包裝,并且還提供了幾個很有用的工具函數(shù)讓我們能更好地使用包裝錯誤。這篇文章就來主要說一下這方面的知識點,不過開始我們還是再次強調(diào)一下使用 Go Error 的誤區(qū),避免我們從其他語言切換過來時給自己后面挖坑。
自定義錯誤要實現(xiàn)error接口
這一條估計很多人都知道,但是文章開頭開始先從這個慣例開始,因為我以前待過一個PHP轉(zhuǎn)Go的研發(fā)團隊,可能大家一開始都不太會,才有了這種錯誤的使用方式。
首先我們再復(fù)述一遍,Go?通過error類型的值表示程序里的錯誤。
error?類型是一個內(nèi)建接口類型,該接口只規(guī)定了一個返回字符串值的Error方法。
type error interface {
Error() string
}Go?程序的函數(shù)經(jīng)常會返回一個error值
package strconv
func Atoi(s string) (int, error) {
....
}
調(diào)用者通過測試error?值是否是nil來進行錯誤處理。
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)error為nil?時表示成功;非nil的error表示失敗。
說完 Go? 里 error 最基本的使用方式后,接下來說項目里的自定義錯誤類型。假如項目在 Dao 層定義了一個這樣的錯誤類型來記錄數(shù)據(jù)庫查詢錯誤。
type MyError struct {
Sql string
Param string
Err error
}假如,這個自定義的MyError?不去實現(xiàn)error?接口,Dao 層里的函數(shù)返回的都是MyError的話。
func FindUserRowByPhoneMyError(userId int) (user User, MyError error) {
......
}那么使用這些 Dao 函數(shù)的代碼邏輯層都得引入dao.MyError?這個額外的類型。有人會說,我把MyError?定義在公共包里,所有代碼邏輯層、Dao 層都用這個common.MyError總沒啥問題了吧。
使用上乍一看沒什么問題,但其實最大的問題就是不兼容、不符合Go語言對錯誤的接口約束,就沒法對自定義錯誤類型使用Go對error提供的其他功能了,比如說后面要介紹的錯誤包裝。
所以針對自定義的錯誤類型,我們也要讓他變成一個真正的Go error,方法就是讓它實現(xiàn)error接口定義的方法。
func (e *MyError) Error() string {
return fmt.Sprintf("sql: %s, params: %s, err: %s", e.Sql, e.Param, e.Err.Error())
}
包裝錯誤
在現(xiàn)實的程序應(yīng)用里,一個邏輯往往要經(jīng)多多層函數(shù)的調(diào)用才能完成,那在程序里我們的建議Error Handling 盡量留給上層的調(diào)用函數(shù)做,中間和底層的函數(shù)通過錯誤包裝把自己要記的錯誤信息附加再原始錯誤上再返回給外層函數(shù)。
比如像下面這樣:
func doAnotherThing() error {
return errors.New("error doing another thing")
}
func doSomething() error {
err := doAnotherThing()
return fmt.Errorf("error doing something: %v", err)
}
func main() {
err := doSomething()
fmt.Println(err)
}這段代碼從打印錯誤信息的輸出上看沒什么問題,但是深層次的問題很明顯,我們丟失了原來的err?,因為它已經(jīng)被我們的fmt.Errorf函數(shù)轉(zhuǎn)成一個新的字符串了。
基于這個背景,很多開源三方庫提供了錯誤包裝、追加錯誤調(diào)用棧等功能,用的最多的就是"github.com/pkg/errors"這個庫,提供了下面幾個主要的包裝錯誤的功能。
//只附加新的信息
func WithMessage(err error, message string) error
//只附加調(diào)用堆棧信息
func WithStack(err error) error
//同時附加堆棧和信息
func Wrap(err error, message string) error
Go官方在2019年發(fā)布1.13?版本,自己也增加了對錯誤包裝的支持,不過并沒有提供什么Wrap?函數(shù),而是擴展了fmt.Errorf?函數(shù),加了一個%w來生成一個包裝錯誤。
e := errors.New("原始錯誤")
w := fmt.Errorf("外面包了一個錯誤%w", e)Go1.13?引入了包裝錯誤后,同時為內(nèi)置的errors?包添加了3個函數(shù),分別是Unwrap、Is和As。
先來聊聊Unwrap,顧名思義,它的功能就是為了獲取到包裝錯誤里那個被嵌套的error。
func Unwrap(err error) error {
//先判斷是否是wrapping error
u, ok := err.(interface {
Unwrap() error
})
//如果不是,返回nil
if !ok {
return nil
}
//否則則調(diào)用該error的Unwrap方法返回被嵌套的error
return u.Unwrap()
}
這里需要注意的是,嵌套可以有很多層,我們調(diào)用一次errors.Unwrap?函數(shù)只能返回往里一層的error?,如果想獲取更里面的,需要調(diào)用多次errors.Unwrap?函數(shù)。最終如果一個error?不是warpping error,那么返回的是nil。
如果想得到最原始的error,建議自己封裝個工具函數(shù),類似這樣
func Cause(err error) error {
for err != nil {
err = errors.Unwrap(err)
}
return err
}對于我們文章開頭定義的那個自定義錯誤MyError?想要把它變成可包裝的Error的話,還需要實現(xiàn)一個Unwrap()方法。
func (e *MyError) Unwrap() error { return e.Err }有了包裝錯誤后,像具體某種錯誤的判斷和錯誤的類型轉(zhuǎn)換也得需要跟進改一下才行。這就是errors?包在1.13?后新增的另外兩個工具函數(shù)Is和As的作用。接下來我們一個個來說。
errors.Is
在Go 1.13之前沒有包裝錯誤的時候,程序里要判斷是不是同一個error可以直接簡單粗暴的:
if err == os.ErrNotExists {
......
}這樣我們就可以通過判斷來做一些事情。但是現(xiàn)在有了包裝錯誤后這樣辦法就不完美的,因為你根本不知道返回的這個err?是不是一個嵌套的error,嵌套了幾層。所以基于這種情況,Go為我們提供了errors.Is函數(shù)。
func Is(err, target error) bool
如果err?和目標(biāo)錯誤target?是同一個,那么返回true。
如果err? 是一個包裝錯誤,目標(biāo)錯誤target?也包含在這個嵌套錯誤鏈中的話,那么也返回true。
下面是一個使用errors.Is判斷是否是同一錯誤的例子。
var ErrDivideByZero = errors.New("divide by zero")
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
switch {
case errors.Is(err, ErrDivideByZero):
fmt.Println("divide zero error")
default:
fmt.Printf("unexpected division error: %+v\n", err)
}
return
}
fmt.Printf("%d / %d = %d\n", a, b, result)
}
errors.As
同樣在沒有包裝錯誤前,我們要把error 轉(zhuǎn)換為一個具體類型的error,一般都是使用類型斷言或者 type switch,其實也就是類型斷言。
if pathErr, ok := err.(*os.PathError); ok {
fmt.Println(pathErr.Path)
}但是有了包裝錯誤之后,返回的err可能是已經(jīng)被嵌套了,這種方式就不能用了,所以Go為我們在errors?包里提供了As函數(shù)。
func As(err error, target interface{}) boolAs? 函數(shù)所做的就是遍歷錯誤的嵌套鏈,從里面找到類型符合的error,然后把這個error賦給target參數(shù),這樣我們在程序里就可以使用轉(zhuǎn)換后的target了,因為這里有賦值,所以target必須是一個指針,這個也算是Go內(nèi)置包里的一個慣例了,像json.Unmarshal也是這樣。
所以把上面的例子用As 函數(shù)實現(xiàn)就變成了醬嬸:
var pathErr *os.PathError
if errors.As(err, pathErr) {
fmt.Println(pathErr.Path)
}
總結(jié)
這篇文章主要是更新一下Error處理在Go 1.13以后新增的功能點,以前的文章介紹的更多的還是使用"pkg/errors"那個包的方式,主要是前兩年以前公司用的Go版本一直是1.12,所以這部分知識我一直沒更新過來,這里簡單做個梳理。
本文標(biāo)題:Go版本大于1.13,程序里這樣做錯誤處理才地道
瀏覽路徑:http://www.fisionsoft.com.cn/article/djosogs.html


咨詢
建站咨詢
