<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          3種方式!Go Error處理最佳實(shí)踐

          共 17721字,需瀏覽 36分鐘

           ·

          2022-08-29 12:05

          導(dǎo)語?| ? 錯(cuò)誤處理一直以一是編程必需要面對(duì)的問題,錯(cuò)誤處理如果做的好的話,代碼的穩(wěn)定性會(huì) 很好。不同的語言有不同的出現(xiàn)處理的方式。Go語言也一樣,在本篇文章中,我們來討論一下Go語言的錯(cuò)誤處理方式。


          一、錯(cuò)誤與異常


          (一) Error


          錯(cuò)誤是程序中可能出現(xiàn)的問題,比如連接數(shù)據(jù)庫失敗,連接網(wǎng)絡(luò)失敗等,在程序設(shè)計(jì)中,錯(cuò)誤處理是業(yè)務(wù)的一部分。


          Go內(nèi)建一個(gè)error接口類型作為go的錯(cuò)誤標(biāo)準(zhǔn)處理


          http://golang.org/pkg/builtin/#error


                
                  
                    // 接口定義
                  
                
                
                  type error interface {
                
                
                     Error() string
                
                
                  }
                
              


          http://golang.org/src/pkg/errors/errors.go


                
                  
                    // 實(shí)現(xiàn)
                  
                
                
                  func New(text string) error {
                
                
                     return &errorString{text}
                
                
                  }
                
                
                  
                    
          type errorString struct { s string }
          func (e *errorString) Error() string { return e.s }



          (二)Exception


          異常是指在不該出現(xiàn)問題的地方出現(xiàn)問題,是預(yù)料之外的,比如空指針引用,下標(biāo)越界,向空map添加鍵值等。


          • 人為制造被自動(dòng)觸發(fā)的異常,比如:數(shù)組越界,向空map添加鍵值對(duì)等。


          • 手工觸發(fā)異常并終止異常,比如:連接數(shù)據(jù)庫失敗主動(dòng)panic。



          (三)panic


          對(duì)于真正意外的情況,那些表示不可恢復(fù)的程序錯(cuò)誤,不可恢復(fù)才使用panic。對(duì)于其他的錯(cuò)誤情況,我們應(yīng)該是期望使用error來進(jìn)行判定。


          go源代碼很多地方寫panic, 但是工程實(shí)踐業(yè)務(wù)代碼不要主動(dòng)寫panic,理論上panic只存在于server啟動(dòng)階段,比如config文件解析失敗,端口監(jiān)聽失敗等等,所有業(yè)務(wù)邏輯禁止主動(dòng)panic,所有異步的goroutine都要用recover去兜底處理。



          (四)總結(jié)


          理解了錯(cuò)誤和異常的真正含義,我們就能理解Go的錯(cuò)誤和異常處理的設(shè)計(jì)意圖。傳統(tǒng)的try...catch...結(jié)構(gòu),很容易讓開發(fā)人員把錯(cuò)誤和異常混為一談,甚至把業(yè)務(wù)錯(cuò)誤處理的一部分當(dāng)做異常來處理,于是你會(huì)在程序中看到一大堆的catch...


          Go開發(fā)團(tuán)隊(duì)認(rèn)為錯(cuò)誤應(yīng)該明確地當(dāng)成業(yè)務(wù)的一部分,任何可以預(yù)見的問題都需要做錯(cuò)誤處理,于是在Go代碼中,任何調(diào)用者在接收函數(shù)返回值的同時(shí)也需要對(duì)錯(cuò)誤進(jìn)行處理,以防遺漏任何運(yùn)行時(shí)可能的錯(cuò)誤。


          異常則是意料之外的,甚至你認(rèn)為在編碼中不可能發(fā)生的,Go遇到異常會(huì)自動(dòng)觸發(fā)panic(恐慌),觸發(fā)panic程序會(huì)自動(dòng)退出。除了程序自動(dòng)觸發(fā)異常,一些你認(rèn)為不可允許的情況你也可以手動(dòng)觸發(fā)異常。


          另外,在Go中除了觸發(fā)異常,還可以終止異常并可選的對(duì)異常進(jìn)行錯(cuò)誤處理,也就是說,錯(cuò)誤和異常是可以相互轉(zhuǎn)換的。



          二、Go處理錯(cuò)誤的三種方式


          (一)經(jīng)典Go邏輯


          直觀的返回error:


                
                  type ZooTour interface {
                
                
                      Enter() error 
                
                
                      VisitPanda(panda *Panda) error 
                
                
                      Leave() error
                
                
                  }
                
                
                  
                    
          // 分步處理,每個(gè)步驟可以針對(duì)具體返回結(jié)果進(jìn)行處理 func Tour(t ZooTour1, panda *Panda) error { if err := t.Enter(); err != nil { return errors.WithMessage(err, "Enter failed.") } if err := t.VisitPanda(); err != nil { return errors.WithMessage(err, "VisitPanda failed.") } // ...
          return nil }



          (二)屏蔽過程中的error的處理


          將error保存到對(duì)象內(nèi)部,處理邏輯交給每個(gè)方法,本質(zhì)上仍是順序執(zhí)行。標(biāo)準(zhǔn)庫的bufio、database/sql包中的Rows等都是這樣實(shí)現(xiàn)的,有興趣可以去看下源碼:


                
                  type ZooTour interface {
                
                
                      Enter() error
                
                
                      VisitPanda(panda *Panda) error
                
                
                      Leave() error
                
                
                      Err() error
                
                
                  }
                
                
                  
                    
          func Tour(t ZooTour, panda *Panda) error {
          t.Enter() t.VisitPanda(panda) t.Leave() // 集中編寫業(yè)務(wù)邏輯代碼,最后統(tǒng)一處理error if err := t.Err(); err != nil { return errors.WithMessage(err, "ZooTour failed") } return nil }



          (三)利用函數(shù)式編程延遲運(yùn)行


          分離關(guān)注點(diǎn)-遍歷訪問用數(shù)據(jù)結(jié)構(gòu)定義運(yùn)行順序,根據(jù)場景選擇,如順序、逆序、二叉樹樹遍歷等。運(yùn)行邏輯將代碼的控制流邏輯抽離,靈活調(diào)整。kubernetes中的visitor對(duì)此就有很多種擴(kuò)展方式,分離了數(shù)據(jù)和行為,有興趣可以去擴(kuò)展閱讀:


                
                  type Walker interface {
                
                
                      Next MyFunc
                
                
                  }
                
                
                  type SliceWalker struct {
                
                
                      index int 
                
                
                      funs []MyFunc
                
                
                  } 
                
                
                  
                    
          func NewEnterFunc() MyFunc { return func(t ZooTour) error { return t.Enter() } }
          func BreakOnError(t ZooTour, walker Walker) error { for { f := walker.Next() if f == nil { break } if err := f(t); err := nil { // 遇到錯(cuò)誤break或者continue繼續(xù)執(zhí)行 } } }



          (四)三種方式對(duì)比


          上面這三個(gè)例子,是Go項(xiàng)目處理錯(cuò)誤使用頻率最高的三種方式,也可以應(yīng)用在error以外的處理邏輯。


          • case1: 如果業(yè)務(wù)邏輯不是很清楚,比較推薦case1;


          • case2: 代碼很少去改動(dòng),類似標(biāo)準(zhǔn)庫,可以使用case2;


          • case3: 比較復(fù)雜的場景,復(fù)雜到抽象成一種設(shè)計(jì)模式。



          三、分層下的Error Handling


          (一)一個(gè)常見的三層調(diào)用


          在工程實(shí)踐中,以一個(gè)常見的三層架構(gòu)(dao->service->controller)為例,我們常見的錯(cuò)誤處理方式大致如下:


                
                  
                    // controller
                  
                
                
                  if err := mode.ParamCheck(param); err != nil {
                
                
                      log.Errorf("param=%+v", param)
                
                
                      return errs.ErrInvalidParam
                
                
                  }
                
                
                  
                    
          return mode.ListTestName("")
          // service _, err := dao.GetTestName(ctx, settleId) if err != nil { log.Errorf("GetTestName failed. err: %v", err) return errs.ErrDatabase }
          // dao if err != nil { log.Errorf("GetTestDao failed. uery: %s error(%v)", sql, err) }



          (二)問題總結(jié)


          • 分層開發(fā)導(dǎo)致的處處打印日志;


          • 難以獲取詳細(xì)的堆棧關(guān)聯(lián);


          • 根因丟失。



          (三)Wrap erros


          Go相關(guān)的錯(cuò)誤處理方法很多,但大多為過渡方案,這里就不一一分析了(類似 github.com/juju/errors 庫,有興趣可以了解)。這里我以 github.com/pkg/errors 為例,這個(gè)也是官方Proposal的重點(diǎn)參考對(duì)象。


          1. 錯(cuò)誤要被日志記錄;


          2. 應(yīng)用程序處理錯(cuò)誤,保證100%完整性;


          3. 之后不再報(bào)告當(dāng)前錯(cuò)誤(錯(cuò)誤只被處理一次)。


          github.com/pkg/errors包主要包含以下幾個(gè)方法,如果我們要新生成一個(gè)錯(cuò)誤,可以使用New函數(shù),生成的錯(cuò)誤,自帶調(diào)用堆棧信息。如果有一個(gè)現(xiàn)成的error ,我們需要對(duì)他進(jìn)行再次包裝處理,這時(shí)候有三個(gè)函數(shù)可以選擇(WithMessage/WithStack/Wrapf)。其次,如果需要對(duì)源錯(cuò)誤類型進(jìn)行自定義判斷可以使用Cause,可以獲得最根本的錯(cuò)誤原因。


                
                  
                    // 新生成一個(gè)錯(cuò)誤, 帶堆棧信息
                  
                
                
                  func New(message string) error
                
                
                  
                    
          // 只附加新的信息 func WithMessage(err error, message string) error
          // 只附加調(diào)用堆棧信息 func WithStack(err error) error
          // 同時(shí)附加堆棧和信息 func Wrapf(err error, format string, args ...interface{}) error
          // 獲得最根本的錯(cuò)誤原因 func Cause(err error) error


          以常見的一個(gè)三層架構(gòu)為例:


          a0f5449e4e8a93d8f3784ffeba5f6dba.webp


          • Dao層使用Wrap上拋錯(cuò)誤


                
                      if err != nil {
                
                
                          if errors.Is(err, sql.ErrNoRows) {
                
                
                              return nil, errors.Wrapf(ierror.ErrNotFound, "query:%s", query)
                
                
                          }
                
                
                          return nil, errors.Wrapf(ierror.ErrDatabase,
                
                
                              "query: %s error(%v)", query, err)
                
                
                      }
                
              


          • Service層追加信息


                
                      bills, err := a.Dao.GetName(ctx, param)
                
                
                      if err != nil {
                
                
                          return result, errors.WithMessage(err, "GetName failed")
                
                
                      }
                
              


          • MiddleWare統(tǒng)一打印錯(cuò)誤日志

                
                  
                    // 請(qǐng)求響應(yīng)組裝
                  
                
                
                  func (Format) Handle(next ihttp.MiddleFunc) ihttp.MiddleFunc {
                
                
                      return func(ctx context.Context, req *http.Request, rsp *ihttp.Response) error {
                
                
                          format := &format{Time: time.Now().Unix()}
                
                
                          err := next(ctx, req, rsp)
                
                
                          format.Data = rsp.Data
                
                
                          if err != nil {
                
                
                              format.Code, format.Msg = errCodes(ctx, err)
                
                
                          }
                
                
                          rsp.Data = format
                
                
                          return nil
                
                
                      }
                
                
                  }
                
                
                  
                    
          // 獲取錯(cuò)誤碼 func errCodes(ctx context.Context, err error) (int, string) { if err != nil { log.CtxErrorf(ctx, "error: [%+v]", err) } var myError = new(erro.IError) if errors.As(err, &myError) { return myError.Code, myError.Msg }
          return code.ServerError, i18n.CodeMessage(code.ServerError) }


          • 和其他庫進(jìn)行協(xié)作


          如果和其他庫進(jìn)行協(xié)作,考慮使用errors.Wrap或者errors.Wrapf保存堆棧信息。同樣適用于和標(biāo)準(zhǔn)庫協(xié)作的時(shí)候。


                
                  _, err := os.Open(path)
                
                
                  if err != nil {
                
                
                     return errors.Wrapf(err, "Open failed. [%s]", path)
                
                
                  }
                
              


          • 包內(nèi)如果調(diào)用其他包內(nèi)的函數(shù),通常簡單的直接return err


          最終效果樣例:


          5d2c3286788a9b4f57e6fef2d9d2a13b.webp


          關(guān)鍵點(diǎn)總結(jié):


          • MyError作為全局error的底層實(shí)現(xiàn),保存具體的錯(cuò)誤碼和錯(cuò)誤信息;


          • MyError向上返回錯(cuò)誤時(shí),第一次先用Wrap初始化堆棧,后續(xù)用WithMessage增加堆棧信息;


          • 要判斷error是否為指定的錯(cuò)誤時(shí),可以使用errors.Cause獲取root error,再進(jìn)行和sentinel error判定;


          • github.com/pkg/errors和標(biāo)準(zhǔn)庫的error完全兼容,可以先替換、后續(xù)改造歷史遺留的代碼;


          • 打印error的堆棧需要用%+v,而原來的%v依舊為普通字符串方法;同時(shí)也要注意日志采集工具是否支持多行匹配;


          • log error級(jí)別的打印棧,warn和info可不打印堆棧;


          • 可結(jié)合統(tǒng)一錯(cuò)誤碼使用:

            https://google-cloud.gitbook.io/api-design-guide/errors



          四、errgroup集中錯(cuò)誤處理


          官方的ErrGroup非常簡單,其實(shí)就是解決小型多任務(wù)并發(fā)任務(wù)。基本用法golang.org/x/sync/errgroup包下定義了一個(gè)Group struct,它就是我們要介紹的ErrGroup并發(fā)原語,底層也是基于WaitGroup實(shí)現(xiàn)的。在使用ErrGroup時(shí),我們要用到三個(gè)方法,分別是WithContext、Go和Wait。


          (一)背景


          通常,在寫業(yè)務(wù)代碼性能優(yōu)化時(shí)經(jīng)常將一個(gè)通用的父任務(wù)拆成幾個(gè)小任務(wù)并發(fā)執(zhí)行。此時(shí)需要將一個(gè)大的任務(wù)拆成幾個(gè)小任務(wù)并發(fā)執(zhí)行,來提高QPS,我們需要再業(yè)務(wù)代碼里嵌入以下邏輯,但這種方式存在問題:


          • 每個(gè)請(qǐng)求都開啟goroutinue,會(huì)有一定的性能開銷。


          • 野生的goroutinue,生命周期管理比較困難。


          • 收到類似SIGQUIT信號(hào)時(shí),無法平滑退出。



          (二)errgroup函數(shù)簽名


                
                  type Group
                
                
                      func WithContext(ctx context.Context) (*Group, context.Context)
                
                
                      func (g *Group) Go(f func() error)
                
                
                      func (g *Group) Wait() error
                
              


          整個(gè)包就一個(gè)Group結(jié)構(gòu)體:


          • 通過WithContext可以創(chuàng)建一個(gè)帶取消的Group;


          • 當(dāng)然除此之外也可以零值的Group也可以直接使用,但是出錯(cuò)之后就不會(huì)取消其他的goroutine了;


          • Go方法傳入一個(gè)func() error內(nèi)部會(huì)啟動(dòng)一個(gè)goroutine去處理;


          • Wait類似WaitGroup的Wait方法,等待所有的goroutine結(jié)束后退出,返回的錯(cuò)誤是一個(gè)出錯(cuò)的err。



          (三)使用案例


          注意這里有一個(gè)坑,在后面的代碼中不要把ctx當(dāng)做父 context又傳給下游,因?yàn)閑rrgroup取消了,這個(gè)context就沒用了,會(huì)導(dǎo)致下游復(fù)用的時(shí)候出錯(cuò)


                
                  func TestErrgroup() {
                
                
                     eg, ctx := errgroup.WithContext(context.Background())
                
                
                     for i := 0; i < 100; i++ {
                
                
                        i := i
                
                
                        eg.Go(func() error {
                
                
                           time.Sleep(2 * time.Second)
                
                
                           select {
                
                
                           case <-ctx.Done():
                
                
                              fmt.Println("Canceled:", i)
                
                
                              return nil
                
                
                           default:
                
                
                              fmt.Println("End:", i)
                
                
                              return nil
                
                
                           }})}
                
                
                     if err := eg.Wait(); err != nil {
                
                
                        log.Fatal(err)
                
                
                     }
                
                
                  }
                
              



          (四)errgroup拓展包


          B站拓展包

          (https://github.com/go-kratos/kratos/blob/v0.3.3/pkg/sync/errgroup/errgroup.go)


          相比官方的結(jié)構(gòu),B站的結(jié)構(gòu)多出了一個(gè)函數(shù)簽名管道和一個(gè)函數(shù)簽名切片,并把Context直接放入了返回的Group結(jié)構(gòu),返回僅返回一個(gè)Group結(jié)構(gòu)指針。

          ?

                
                  type Group struct {
                
                
                     err     error
                
                
                     wg      sync.WaitGroup
                
                
                     errOnce sync.Once
                
                
                  
                    
          workerOnce sync.Once ch chan func(ctx context.Context) error chs []func(ctx context.Context) error
          ctx context.Context cancel func() }
          func WithContext(ctx context.Context) *Group { return &Group{ctx: ctx} }


          Go方法可以看出并不是直接起協(xié)程的(如果管道已經(jīng)初始化好了),而是優(yōu)先將函數(shù)簽名放入管道,管道如果滿了就放入切片。


                
                  func (g *Group) Go(f func(ctx context.Context) error) {
                
                
                     g.wg.Add(1)
                
                
                     if g.ch != nil {
                
                
                        select {
                
                
                        case g.ch <- f:
                
                
                        default:
                
                
                           g.chs = append(g.chs, f)
                
                
                        }
                
                
                        return
                
                
                     }
                
                
                     go g.do(f)
                
                
                  }
                
              


          GOMAXPROCS函數(shù)其實(shí)是起了一個(gè)并發(fā)池來控制協(xié)程數(shù)量,傳入最大協(xié)程數(shù)量進(jìn)行并發(fā)消費(fèi)管道里的函數(shù)簽名:


                
                  func (g *Group) GOMAXPROCS(n int) {
                
                
                     if n <= 0 {
                
                
                        panic("errgroup: GOMAXPROCS must great than 0")
                
                
                     }
                
                
                     g.workerOnce.Do(func() {
                
                
                        g.ch = make(chan func(context.Context) error, n)
                
                
                        for i := 0; i < n; i++ {
                
                
                           go func() {
                
                
                              for f := range g.ch {
                
                
                                 g.do(f)
                
                
                              }
                
                
                           }()
                
                
                        }
                
                
                     })
                
                
                  }
                
              


          整個(gè)流程梳理下來其實(shí)就是啟動(dòng)一個(gè)固定數(shù)量的并發(fā)池消費(fèi)任務(wù),Go函數(shù)其實(shí)是向管道中發(fā)送任務(wù)的生產(chǎn)者,這個(gè)設(shè)計(jì)中有意思的是他的協(xié)程生命周期的控制,他的控制方式是每發(fā)送一個(gè)任務(wù)都進(jìn)行WaitGroup加一,在最后結(jié)束時(shí)的wait函數(shù)中進(jìn)行等待,等待所有的請(qǐng)求都處理完才會(huì)關(guān)閉管道,返出錯(cuò)誤。


          tips :


          • B站拓展包主要解決了官方ErrGroup的幾個(gè)痛點(diǎn): 制并發(fā)量、 Recover住協(xié)程的Panic并打出堆棧信息。


          • Go方法并發(fā)的去調(diào)用在量很多的情況下會(huì)產(chǎn)生死鎖,因?yàn)樗那衅皇蔷€程安全的,如果要并發(fā),并發(fā)數(shù)量一定不能過大,一旦動(dòng)用了任務(wù)切片,那么很有可能就在wait方法那里hold住了。這個(gè)可以加個(gè)鎖來優(yōu)化。


          • Wg watigroup只在Go方法中進(jìn)行Add(),并沒有控制消費(fèi)者的并發(fā),Wait的邏輯就是分發(fā)者都分發(fā)完成,直接關(guān)閉管道,讓消費(fèi)者并發(fā)池自行銷毀,不去管控,一旦邏輯中有完全hold住的方法那么容易產(chǎn)生內(nèi)存泄漏。


          ?作者簡介


          60b2d8491cc0fbf55432d3ac3eb188fa.webp

          李森林

          騰訊后臺(tái)工程師

          騰訊后臺(tái)工程師,目前負(fù)責(zé)騰訊游戲內(nèi)容平臺(tái)的設(shè)計(jì)、開發(fā)和維護(hù)工作。



          推薦閱讀


          福利
          我為大家整理了一份 從入門到進(jìn)階的Go學(xué)習(xí)資料禮包 ,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。 關(guān)注公眾號(hào) 「polarisxu」,回復(fù)? ebook ?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。


          瀏覽 32
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉俺来也 | 蜜桃在线码无精品秘 入口九色 | 99爱逼| 亚洲欧美日本在线观看 | 欧美精品一区二区少妇免费A片 |