<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í)踐

          共 6618字,需瀏覽 14分鐘

           ·

          2022-05-10 09:04


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


          一、錯(cuò)誤與異常


          (一)Error


          錯(cuò)誤是程序中可能出現(xiàn)的問題,比如連接數(shù)據(jù)庫(kù)失敗,連接網(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ù)庫(kù)失敗主動(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)庫(kù)的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ù)場(chǎng)景選擇,如順序、逆序、二叉樹樹遍歷等。運(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)庫(kù),可以使用case2;


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



          三、分層下的Error Handling


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


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


          // controllerif 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}
          // daoif 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庫(kù),有興趣可以了解)。這里我以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)為例:



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


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


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


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


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


          最終效果樣例:



          關(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)庫(kù)的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非常簡(jiǎn)單,其實(shí)就是解決小型多任務(wù)并發(fā)任務(wù)?;居梅╣olang.org/x/sync/errgroup包下定義了一個(gè)Group struct,它就是我們要介紹的ErrGroup并發(fā)原語(yǔ),底層也是基于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)存泄漏。


          ?作者簡(jiǎn)介


          李森林

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

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



          ?推薦閱讀


          生于云,長(zhǎng)于云,開發(fā)者如何更好地吃透云原生?

          從0到1詳解ZooKeeper的應(yīng)用場(chǎng)景及架構(gòu)原理!

          分布式事務(wù)解決方案:從了解到放棄!

          Go語(yǔ)言從0到1實(shí)現(xiàn)最簡(jiǎn)單的數(shù)據(jù)庫(kù)!



          瀏覽 49
          點(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>
                  色婷婷一区二区三区久久午夜成人 | 国产婬片一级A片AAA毛片AⅤ | 在线免费观看亚洲视频 | 亚洲午夜无码久久久久蜜桃AV | 中文字幕日本欧美 |