Golang如何優雅地處理錯誤和日誌

很多人吐槽Go語言錯誤處理太繁瑣了,程式碼裡面到處都是錯誤判斷”

if err != nil

“。

Go語言錯誤是透過返回值,強迫呼叫者立即對錯誤進行處理。要麼忽略,要麼立即處理。相信大家在平時開發也好,在開源專案中也好都能看到 到處都是 if err != nil的判斷。我們開發時要注意:不要只檢查錯誤,要優雅地處理錯誤,我們可以透過消除錯誤來消除錯誤。接下來我們就來一起來優雅地處理錯誤。

透過消除錯誤來優雅地處理錯誤,避免到處是if err != nil

Wrap Errors

如何優雅的Wrap Errors

如何優雅地處理日誌

錯誤總結

1.透過消除錯誤來優雅地處理錯誤,避免到處是if err != nil

1。1。方法簽名一致直接返回就好

1。2。如果一個函式或方法太複雜,內部需要呼叫多個方法。可以提供一個Err()方法在最後呼叫Err()返回錯誤

1.1.方法簽名一致直接返回就好

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

1.2.如果一個函式或方法太複雜,內部需要呼叫多個方法。可以提供一個Err()方法在最後呼叫Err()返回錯誤

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) { sc := bufio。NewScanner(r) lines := 0 for sc。Scan() { lines++ } return lines, sc。Err()}

透過以上2個示例發現,每個方法呼叫就這樣處理,就成了大家吐槽,Go語言到處都是if err != nil。小夥伴不信的話去看 Go 很多專案都是這樣寫,現在可能好點了,早起的Go專案大多數都寫的。

2.Wrap Errors

為什麼要使用 wrap error

1。底層丟擲來了一個 error 你不知道是一個什麼錯誤從哪裡丟擲來的,因為缺少了上下文。

2。有時候我們希望保留原始錯誤,又想攜帶更多的資訊。

Wrap Error 是 Go

1。13中新增的特性,為了讓我們優雅地處理業務邏輯中的錯誤。因為一些歷史原因,go 1。13 wrap error不支援獲取堆疊資訊。後面我們一起使用”pkg/errors“和 go 1。13結合來處理錯誤。

2。1。如何生成Wrap Error

2。2。 Is()方法

2。3。 As() 方法

2。4。 Unwrap() 方法

2。5。 github。com/pkg/errors包與go 1。13 error結合使用

2.1.如何生成Wrap Error

fmt。Errorf():包裝一個錯誤

// 包裹錯誤err := errors。New(“raw error”)errWrap1 := fmt。Errorf(“wrap1 \n%w”, err)errWrap2 := fmt。Errorf(“wrap2 \n%w”, errWrap1)fmt。Println(errWrap1)fmt。Println()fmt。Println(errWrap2)/*輸出:wrap1 raw errorwrap2 wrap1 raw error*/

2.2. Is()方法

當錯誤被 Wrap Error後我們上層沒辦法知道底層錯誤是什麼,Is方法就是做這個事的。

Is()方法:檢測 一個 error 是否包含指定的錯誤

// errors。Is(err, target error) boolerr := errors。New(“raw error”)errWrap1 := fmt。Errorf(“wrap1 \n%w”, err)errWrap2 := fmt。Errorf(“wrap2 \n%w”, errWrap1)fmt。Println(errors。Is(errWrap1, err))// truefmt。Println(errors。Is(errWrap2, errWrap1))// truefmt。Println(errors。Is(errWrap2, errors。New(“raw error”)))// false

2.3. As() 方法

當我們使用 Wrap Error錯誤後,沒辦法直接使用斷言去判斷底層錯誤是啥。

As():把錯誤轉換成指定的目標錯誤,類似於斷言。

// errors。As(err error, target interface{}) boolerr := errors。New(“raw error”)errWrap1 := fmt。Errorf(“wrap1 \n%w”, err)errWrap2 := fmt。Errorf(“wrap2 \n%w”, errWrap1)errWrapAs := errors。New(“raw error”)var perr *os。PathErrorfmt。Println(errors。As(errWrap2, &errWrapAs))// truefmt。Println(errors。As(errWrap2, &perr))// false

2.4. Unwrap() 方法

Unwrap() :返回外層的error

// errors。Unwrap(err error) errorerr := errors。New(“raw error”)errWrap1 := fmt。Errorf(“wrap1 \n%w”, err)errWrap2 := fmt。Errorf(“wrap2 \n%w”, errWrap1)fmt。Println(errors。Unwrap(errWrap2))fmt。Println()fmt。Println(errors。Unwrap(errors。Unwrap(errWrap2)))/*wrap1 raw errorraw error*/

2.5. github.com/pkg/errors包與go 1.13 error結合使用

”pkg/errors“對go 1。13 error做了相容,所以我們可以直接使用”pkg/errors“

Golang如何優雅地處理錯誤和日誌

pkg/errors

pkg/errors。New():建立包含堆疊資訊的錯誤

pkg/errors。Wrap():打包一個包含堆疊資訊的錯誤

pkg/errors。WithMessage():使用Message包裝錯誤錯誤,不包含堆疊資訊

package mainimport ( “fmt” pkgErrors “github。com/pkg/errors”)func main() { err := pkgErrors。New(“raw error”)// 建立錯誤會包含堆疊資訊 fmt。Printf(“%+v”, err)}/**輸出:raw errormain。main /Users/zhaoweijie/work/develop/go/hello/main。go:10runtime。main /usr/local/Cellar/go/1。16/libexec/src/runtime/proc。go:225runtime。goexit /usr/local/Cellar/go/1。16/libexec/src/runtime/asm_amd64。s:1371% */

package mainimport ( “errors” “fmt” pkgErrors “github。com/pkg/errors”)func main() { err := errors。New(“raw error”) wrapErr := pkgErrors。Wrap(err, “wrap 1”) // pkg error wrap 打包錯誤會包含堆疊資訊 fmt。Printf(“%+v”, wrapErr)}/**輸出:raw errorwrap 1main。main /Users/zhaoweijie/work/develop/go/hello/main。go:12runtime。main /usr/local/Cellar/go/1。16/libexec/src/runtime/proc。go:225runtime。goexit /usr/local/Cellar/go/1。16/libexec/src/runtime/asm_amd64。s:1371*/

package mainimport ( “errors” “fmt” pkgErrors “github。com/pkg/errors”)func main() { err := errors。New(“raw error”) wrapErr := pkgErrors。WithMessage(err, “wrap 1”) // 不會包含堆疊資訊 fmt。Printf(“%+v”, wrapErr)}/**輸出:raw errorwrap 1*/

3.如何優雅的Wrap Errors

為了避免多次wrap帶來的效能影響和日誌變得司空見慣,導致日誌中有很多不必要的日誌產生的噪音。

我們應該遵守以下wrap error的使用規則

3。1。在標準庫、第三方庫、基礎庫、kit庫產生的錯誤應該直接返回,而不應該wrap。

3。2。 在應用層程式碼中如果你呼叫了標準庫、第三方和基礎庫產生的錯誤,你應該使用pkg errors。wrap()返回錯誤

3。3。如果你呼叫了本包的其他函式,你不應該wrap

3。4。在應用層程式碼中,如果你判斷某個業務邏輯產生了一個錯誤,你應該使用 pkg errors。New()返回錯誤

3.1.在標準庫、第三方庫、基礎庫、kit庫產生的錯誤應該直接返回,而不應該wrap。

如果在標準庫、第三方庫、基礎庫、kit庫中如果你wrap,呼叫者不知道有沒有wrap,再去wrap了,如果該包被其他包引入,導致被wrap多次。

3.2. 在應用層程式碼中如果你呼叫了標準庫、第三方和基礎庫產生的錯誤,你應該使用pkg errors.wrap()返回錯誤

import ( pkgErrors “github。com/pkg/errors”)func Service() error { _, err := http。Get(“http://notfound。comerr”) return pkgErrors。Wrap(err, “http。get error”)}

3.2.如果你呼叫了本包內的其他函式,你不應該wrap

package serviceimport ( “github。com/pkg/errors”)func service() error { err := serviceDemoFun() // 呼叫了本包(service)的其他函式,你不應該wrap if err != nil { return err } // 業務程式碼 return nil}func serviceDemoFun() error { return errors。New(“service demo fun raw error”)}

3.3.在應用層程式碼中,如果你判斷某個業務邏輯產生了一個錯誤,你應該使用 pkg errors.New()返回錯誤

import ( “github。com/pkg/errors”)func service() error { str := “” if str == “” { return errors。New(“serviceDemoFun result value is empty”) } return nil}

4.如何優雅地處理日誌

4。1。直接返回錯誤,而不是在每個產生錯誤的地方記錄日誌

4。2。錯誤應包含足夠並且有效的上下文資訊

4。3。在應用程式的頂部記錄日誌

4。4。在goroutine的入口處記錄日誌

4.1.直接返回錯誤,而不是在每個產生錯誤的地方記錄日誌

package mainimport ( “fmt” “log” “github。com/pkg/errors”)func main() { wrapErr := service() fmt。Printf(“%+v”, wrapErr)}// 應用層服務func service() error { err := domain() if err != nil { log。Printf(“%s %v”, “service error”, err) } // 業務程式碼 return err}// 業務邏輯層func domain() error { err := dao() if err != nil { log。Printf(“%s %v”, “domain error”, err) } // 業務程式碼 return err}// 資料層func dao() error { err := errors。New(“dao raw error”) log。Printf(“%v”, “dao raw error”) return err}/*2021/08/27 01:18:35 dao raw error2021/08/27 01:18:35 domain error dao raw error2021/08/27 01:18:35 service error dao raw errordao raw errormain。dao /Users/zhaoweijie/work/develop/go/hello/main。go:38main。domain /Users/zhaoweijie/work/develop/go/hello/main。go:27main。service /Users/zhaoweijie/work/develop/go/hello/main。go:17main。main /Users/zhaoweijie/work/develop/go/hello/main。go:11runtime。main /usr/local/Cellar/go/1。16/libexec/src/runtime/proc。go:225runtime。goexit /usr/local/Cellar/go/1。16/libexec/src/runtime/asm_amd64。s:1371% */

修改後

package mainimport ( “log” “github。com/pkg/errors”)func main() { wrapErr := service() log。Printf(“%s %v”, “service error”, wrapErr)}// 應用層服務func service() error { err := domain() // 業務程式碼 return err}// 業務邏輯層func domain() error { err := dao() // 業務程式碼 return err}// 資料層func dao() error { err := errors。New(“dao raw error”) return err}

4.2.只記錄錯誤必要的上下文資訊

1。 比如不應該記錄http介面的響應結果,因為99%以上的錯誤都可以透過請求引數找到錯誤原因

2。比如一個方法有5個引數,但是與錯誤有關的只有2個引數。這時候日誌應該記錄與錯誤有關的2個錯誤引數

4.3.在應用程式的頂部記錄日誌

package mainimport ( “log” “github。com/pkg/errors”)func main() { // 在頂層處理錯誤 wrapErr := service() log。Printf(“%s %v”, “service error”, wrapErr)}// 應用層服務func service() error { return domain()}// 業務邏輯層func domain() error { return dao()}// 資料層func dao() error { return errors。New(“dao raw error”)}

4.4.在goroutine的入口處記錄日誌

package mainimport ( “errors” “log”)func main() { Go(func() error { panic(“goroutine1 panic error”) }) Go(func() error { return errors。New(“goroutine2 error”) }) for { }}func Go(x func() error) { go func() { var err error // 在goroutine的入庫出記錄日誌 defer func() { if errFatal := recover(); errFatal != nil { log。Printf(“%+v”, errFatal) } if err != nil { log。Printf(“%+v”, err) } }() err = x() }()}/**輸出:2021/08/27 01:42:31 goroutine2 error2021/08/27 01:42:31 goroutine1 panic error*/

5.錯誤總結

1。 優先處理失敗,而不是成功2。 完全控制程式碼中的錯誤,做你想做的任何事3。 將錯誤作為一等公民,強迫呼叫者立即處理錯誤4。 沒有隱藏的控制流5。 錯誤應該只處理一次(記錄日誌也算,wrap錯誤也算)6。 如果你吞掉了一個錯誤,你應該保證程式100%的完整性7。 第三方庫、基礎庫、kit庫中產生的錯誤不應該wrap8。 在應用層中呼叫第三方庫產生的產生了一個錯誤應該wrap9。 在應用層中判斷業務邏輯產生了一個錯誤,你應該wrap10。 如果你呼叫了本包的其他函式,你不應該wrap11。 直接返回錯誤,而不是在每個產生錯誤的地方記錄日誌12。 錯誤應該只記錄必要的上下文13。 在應用程式的頂部記錄日誌14。 在goroutine的入口處記錄日誌

對Go語言錯誤和日誌處理,你有其他建議或者不同的意見歡迎在下方留言。