溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Golang的error處理方法有哪些

發(fā)布時間:2022-07-11 13:58:33 來源:億速云 閱讀:301 作者:iii 欄目:開發(fā)技術

這篇文章主要介紹“Golang的error處理方法有哪些”,在日常操作中,相信很多人在Golang的error處理方法有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Golang的error處理方法有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

    Golang中的error

    Golang中的 error 就是一個簡單的接口類型。只要實現(xiàn)了這個接口,就可以將其視為一種 error

    type error interface {
        Error() string
    }

    error的幾種玩法

    翻看Golang源碼,能看到許多類似于下面的這兩種error類型

    哨兵錯誤

    var EOF = errors.New("EOF")
    var ErrUnexpectedEOF = errors.New("unexpected EOF")
    var ErrNoProgress = errors.New("multiple Read calls return no data or error")

    缺點:

    1.讓 error 具有二義性

    error != nil不再意味著一定發(fā)生了錯誤
    比如io.Reader返回io.EOF來告知調用者沒有更多數(shù)據(jù)了,然而這又不是一個錯誤

    2.在兩個包之間創(chuàng)建了依賴

    如果你使用了io.EOF來檢查是否read完所有的數(shù)據(jù),那么代碼里一定會導入io包

    自定義錯誤類型

    一個不錯的例子是os.PathError,它的優(yōu)點是可以附帶更多的上下文信息

    type PathError struct {
        Op   string
        Path string
        Err  error
    }

    Wrap error

    到這里我們可以發(fā)現(xiàn),Golang 的 error 非常簡單,然而簡單也意味著有時候是不夠用的

    Golang的error一直有兩個問題:

    1.error沒有附帶file:line信息(也就是沒有堆棧信息)

    比如這種error,鬼知道代碼哪一行報了錯,Debug時簡直要命

    SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
           Error 1406: Data too long for column 'content' at row 1

    2.上層error想附帶更多日志信息時,往往會使用fmt.Errorf(),fmt.Errorf()會創(chuàng)建一個新的error,底層的error類型就被“吞”掉了

    var errNoRows = errors.New("no rows")
    
    // 模仿sql庫返回一個errNoRows
    func sqlExec() error {
        return errNoRows
    }
    
    func serviceNoErrWrap() error {
        err := sqlExec()
        if err != nil {
            return fmt.Errorf("sqlExec failed.Err:%v", err)
        }
        
        return nil
    }
    
    func TestErrWrap(t *testing.T) {
        // 使用fmt.Errorf創(chuàng)建了一個新的err,丟失了底層err
        err := serviceNoErrWrap()
        if err != errNoRows {
            log.Println("===== errType don't equal errNoRows =====")
        }
    }
    -------------------------------代碼運行結果----------------------------------
    === RUN   TestErrWrap
    2022/03/26 17:19:43 ===== errType don't equal errNoRows =====

    為了解決這個問題,我們可以使用github.com/pkg/error包,使用errors.withStack()方法將err保
    存到withStack對象

    // withStack結構體保存了error,形成了一條error鏈。同時*stack字段保存了堆棧信息。
    type withStack struct {
        error
        *stack
    }

    也可以使用errors.Wrap(err, "自定義文本"),額外附帶一些自定義的文本信息

    源碼解讀:先將err和message包進withMessage對象,再將withMessage對象和堆棧信息包進withStack對象

    func Wrap(err error, message string) error {
        if err == nil {
            return nil
        }
        err = &withMessage{
            cause: err,
            msg:   message,
        }
        return &withStack{
            err,
            callers(),
        }
    }

    Golang1.13版本error的新特性

    Golang1.13版本借鑒了github.com/pkg/error包,新增了如下函數(shù),大大增強了 Golang 語言判斷 error 類型的能力

    errors.UnWrap()

    // 與errors.Wrap()行為相反
    // 獲取err鏈中的底層err
    func Unwrap(err error) error {
        u, ok := err.(interface {
            Unwrap() error
        })
        if !ok {
            return nil
        }
        return u.Unwrap()
    }

    errors.Is()

    在1.13版本之前,我們可以用err == targetErr判斷err類型
    errors.Is()是其增強版:error 鏈上的任一err == targetErr,即return true

    // 實踐:學習使用errors.Is()
    var errNoRows = errors.New("no rows")
    
    // 模仿sql庫返回一個errNoRows
    func sqlExec() error {
        return errNoRows
    }
    
    func service() error {
        err := sqlExec()
        if err != nil {
            return errors.WithStack(err)    // 包裝errNoRows
        }
        
        return nil
    }
    
    func TestErrIs(t *testing.T) {
        err := service()
        
        // errors.Is遞歸調用errors.UnWrap,命中err鏈上的任意err即返回true
        if errors.Is(err, errNoRows) {
            log.Println("===== errors.Is() succeeded =====")
        }
        
        //err經(jīng)errors.WithStack包裝,不能通過 == 判斷err類型
        if err == errNoRows {
            log.Println("err == errNoRows")
        }
    }
    -------------------------------代碼運行結果----------------------------------
    === RUN   TestErrIs
    2022/03/25 18:35:00 ===== errors.Is() succeeded =====

    例子解讀:

    因為使用errors.WithStack包裝了sqlError,sqlError位于error鏈的底層,上層的error已經(jīng)不再是sqlError類型,所以使用==無法判斷出底層的sqlError

    源碼解讀:

    • 我們很容易想到其內部調用了err = Unwrap(err)方法來獲取error鏈中底層的error

    • 自定義error類型可以實現(xiàn)Is接口來自定義error類型判斷方法

    func Is(err, target error) bool {
        if target == nil {
            return err == target
        }
        
        isComparable := reflectlite.TypeOf(target).Comparable()
        for {
            if isComparable && err == target {
                return true
            }
            // 支持自定義error類型判斷
            if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
                return true
            }
            if err = Unwrap(err); err == nil {
                return false
            }
        }
    }

    下面我們來看看如何自定義error類型判斷:

    自定義的errNoRows類型,必須實現(xiàn)Is接口,才能使用erros.Is()進行類型判斷

    type errNoRows struct {
        Desc string
    }
    
    func (e errNoRows) Unwrap() error { return e }
    
    func (e errNoRows) Error() string { return e.Desc }
    
    func (e errNoRows) Is(err error) bool {
        return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
    }
    
    // 模仿sql庫返回一個errNoRows
    func sqlExec() error {
        return &errNoRows{"Kaolengmian NB"}
    }
    
    func service() error {
        err := sqlExec()
        if err != nil {
            return errors.WithStack(err)
        }
        
        return nil
    }
    
    func serviceNoErrWrap() error {
        err := sqlExec()
        if err != nil {
            return fmt.Errorf("sqlExec failed.Err:%v", err)
        }
        
        return nil
    }
    
    func TestErrIs(t *testing.T) {
        err := service()
        
        if errors.Is(err, errNoRows{}) {
            log.Println("===== errors.Is() succeeded =====")
        }
    }
    -------------------------------代碼運行結果----------------------------------
    === RUN   TestErrIs
    2022/03/25 18:35:00 ===== errors.Is() succeeded =====

    errors.As()

    在1.13版本之前,我們可以用if _,ok := err.(targetErr)判斷err類型
    errors.As()是其增強版:error 鏈上的任一err與targetErr類型相同,即return true

    // 通過例子學習使用errors.As()
    type sqlError struct {
        error
    }
    
    func (e *sqlError) IsNoRows() bool {
        t, ok := e.error.(ErrNoRows)
        return ok && t.IsNoRows()
    }
    
    type ErrNoRows interface {
        IsNoRows() bool
    }
    
    // 返回一個sqlError
    func sqlExec() error {
        return sqlError{}
    }
    
    // errors.WithStack包裝sqlError
    func service() error {
        err := sqlExec()
        if err != nil {
            return errors.WithStack(err)
        }
        
        return nil
    }
    
    func TestErrAs(t *testing.T) {
        err := service()
        
        // 遞歸使用errors.UnWrap,只要Err鏈上有一種Err滿足類型斷言,即返回true
        sr := &sqlError{}
        if errors.As(err, sr) {
            log.Println("===== errors.As() succeeded =====")
        }
        
        // 經(jīng)errors.WithStack包裝后,不能通過類型斷言將當前Err轉換成底層Err
        if _, ok := err.(sqlError); ok {
            log.Println("===== type assert succeeded =====")
        }
    }
    ----------------------------------代碼運行結果--------------------------------------------
    === RUN   TestErrAs
    2022/03/25 18:09:02 ===== errors.As() succeeded =====

    例子解讀:

    因為使用errors.WithStack包裝了sqlError,sqlError位于error鏈的底層,上層的error已經(jīng)不再是sqlError類型,所以使用類型斷言無法判斷出底層的sqlError

    error處理最佳實踐

    上面講了如何定義error類型,如何比較error類型,現(xiàn)在我們談談如何在大型項目中做好error處理

    優(yōu)先處理error

    當一個函數(shù)返回一個非空error時,應該優(yōu)先處理error,忽略它的其他返回值

    只處理error一次

    • 在Golang中,對于每個err,我們應該只處理一次。

    • 要么立即處理err(包括記日志等行為),return nil(把錯誤吞掉)。此時因為把錯誤做了降級,一定要小心處理函數(shù)返回值。

    比如下面例子json.Marshal(conf)沒有return err ,那么在使用buf時一定要小心空指針等錯誤

    要么return err,在上層處理err

    反例:

    // 試想如果writeAll函數(shù)出錯,會打印兩遍日志
    // 如果整個項目都這么做,最后會驚奇的發(fā)現(xiàn)我們在處處打日志,項目中存在大量沒有價值的垃圾日志
    // unable to write:io.EOF
    // could not write config:io.EOF
    
    type config struct {}
    
    func writeAll(w io.Writer, buf []byte) error {
        _, err := w.Write(buf)
        if err != nil {
            log.Println("unable to write:", err)
            return err
        }
        
        return nil
    }
    
    func writeConfig(w io.Writer, conf *config) error {
        buf, err := json.Marshal(conf)
        if err != nil {
            log.Printf("could not marshal config:%v", err)
        }
        
        if err := writeAll(w, buf); err != nil {
            log.Println("count not write config: %v", err)
            return err
        }
        
        return nil
    }

    不要反復包裝error

    我們應該包裝error,但只包裝一次

    上層業(yè)務代碼建議Wrap error,但是底層基礎Kit庫不建議

    如果底層基礎 Kit 庫包裝了一次,上層業(yè)務代碼又包裝了一次,就重復包裝了 error,日志就會打重

    比如我們常用的sql庫會返回sql.ErrNoRows這種預定義錯誤,而不是給我們一個包裝過的 error

    不透明的錯誤處理

    在大型項目中,推薦使用不透明的錯誤處理(Opaque errors):不關心錯誤類型,只關心error是否為nil

    好處:

    耦合小,不需要判斷特定錯誤類型,就不需要導入相關包的依賴。
    不過有時候,這種處理error的方式不夠用,比如:業(yè)務需要對參數(shù)異常error類型做降級處理,打印Warn級別的日志

    type ParamInvalidError struct {
        Desc string
    }
    
    func (e ParamInvalidError) Unwrap() error { return e }
    
    func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc }
    
    func (e ParamInvalidError) Is(err error) bool {
        return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
    }
    
    func NewParamInvalidErr(desc string) error {
        return errors.WithStack(&ParamInvalidError{Desc: desc})
    }
    ------------------------------頂層打印日志---------------------------------
    if errors.Is(err, Err.ParamInvalidError{}) {
        logger.Warnf(ctx, "%s", err.Error())
        return
    }
    if err != nil {
        logger.Errorf(ctx, " error:%+v", err)
    }

    簡化錯誤處理

    Golang因為代碼中無數(shù)的if err != nil被詬病,現(xiàn)在我們看看如何減少if err != nil這種代碼

    bufio.scan

    CountLines() 實現(xiàn)了"讀取內容的行數(shù)"功能

    可以利用 bufio.scan() 簡化 error 的處理:

    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, nilsadwawa 
        }
        
        return lines, nil
    }
    
    func CountLinesGracefulErr(r io.Reader) (int, error) {
        sc := bufio.NewScanner(r)
        
        lines := 0
        for sc.Scan() {
            lines++
        }
        
        return lines, sc.Err()
    }

    bufio.NewScanner() 返回一個 Scanner 對象,結構體內部包含了 error 類型,調用Err()方法即可返回封裝好的error

    Golang源代碼中蘊含著大量的優(yōu)秀設計思想,我們在閱讀源碼時從中學習,并在實踐中得以運用

    type Scanner struct {
        r            io.Reader // The reader provided by the client.
        split        SplitFunc // The function to split the tokens.
        maxTokenSize int       // Maximum size of a token; modified by tests.
        token        []byte    // Last token returned by split.
        buf          []byte    // Buffer used as argument to split.
        start        int       // First non-processed byte in buf.
        end          int       // End of data in buf.
        err          error     // Sticky error.
        empties      int       // Count of successive empty tokens.
        scanCalled   bool      // Scan has been called; buffer is in use.
        done         bool      // Scan has finished.
    }
    
    func (s *Scanner) Err() error {
        if s.err == io.EOF {
            return nil
        }
        return s.err
    }

    errWriter

    WriteResponse()函數(shù)實現(xiàn)了"構建HttpResponse"功能

    利用上面學到的思路,我們可以自己實現(xiàn)一個errWriter對象,簡化對 error 的處理

    type Header struct {
        Key, Value string
    }
    
    type Status struct {
        Code   int
        Reason string
    }
    
    func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        if err != nil {
            return err
        }
        
        for _, h := range headers {
            _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
            if err != nil {
                return err
            }
        }
        
        if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
            return err
        }
        
        _, err = io.Copy(w, body)
        return err
    }
    
    type errWriter struct {
        io.Writer
        err error
    }
    
    func (e *errWriter) Write(buf []byte) (n int, err error) {
        if e.err != nil {
            return 0, e.err
        }
        
        n, e.err = e.Writer.Write(buf)
        
        return n, nil
    }
    
    func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {
        ew := &errWriter{w, nil}
        
        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        
        for _, h := range headers {
            fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
        }
        
        fmt.Fprintf(w, "\r\n")
        
        io.Copy(ew, body)
        
        return ew.err
    }

    何時該用panic

    在 Golang 中panic會導致程序直接退出,是一個致命的錯誤。

    建議發(fā)生致命的程序錯誤時才使用 panic,例如索引越界、不可恢復的環(huán)境問題、棧溢出等等

    小補充

    errors.New()返回的是errorString對象的指針,其原因是防止字符串產(chǎn)生碰撞,如果發(fā)生碰撞,兩個 error 對象會相等。
    源碼:

    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
    }

    實踐:error1error2的text都是"error",但是二者并不相等

    func TestErrString(t *testing.T) {
        var error1 = errors.New("error")
        var error2 = errors.New("error")
        
        if error1 != error2 {
            log.Println("error1 != error2")
        }
    }
    ---------------------代碼運行結果--------------------------
    === RUN   TestXXXX
    2022/03/25 22:05:40 error1 != error2

    到此,關于“Golang的error處理方法有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

    AI