1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        也許是 Go Context 最佳實踐

        共 6814字,需瀏覽 14分鐘

         ·

        2021-06-13 00:39

        最早 context 是獨立的第三方庫,后來才移到標(biāo)準(zhǔn)庫里。關(guān)于這個庫該不該用有很多爭義,比如 Context should go away for Go 2[1]. 不管爭義多大,本著務(wù)實的哲學(xué),所有的開源項目都重度使用,當(dāng)然也包括業(yè)務(wù)代碼。

        但是我發(fā)現(xiàn)并不是每個人都了解 context, 從去年到現(xiàn)在就見過兩次因為錯誤使用導(dǎo)致的問題。每個同學(xué)都會踩到坑,今天分享下 context 庫使用的 Dos and Don'ts

        原理

        type Context interface {
         Deadline() (deadline time.Time, ok bool)
         Done() <-chan struct{}
         Err() error
         Value(key interface{}) interface{}
        }

        Context 是一個接口

        1. Deadline ctx 如果在某個時間點關(guān)閉的話,返回該值。否則 ok 為 false
        2. Done 返回一個 channel, 如果超時或是取消就會被關(guān)閉,實現(xiàn)消息通訊
        3. Err 如果當(dāng)前 ctx 超時或被取消了,那么 Err 返回錯誤
        4. Value 根據(jù)某個 key 返回對應(yīng)的 value, 功能類似字典

        目前的實現(xiàn)有 emptyCtx, valueCtx, cancelCtx, timerCtx. 可以基于某個 Parent 派生成 Child Context

        func WithValue(parent Context, key, val interface{}) Context
        func WithCancel(parent Context) (Context, CancelFunc)
        func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
        func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

        這是四個常用的派生函數(shù),WithValue 包裝 key/value 返回 valueCtx, 后三個返回兩個值 Context 是 child ctx, CancelFunc 是取消該 ctx 的函數(shù)?;谶@個特性呢,經(jīng)過多次派生,context 是一個樹形結(jié)構(gòu)

        context tree

        如上圖所示,是一個多叉樹。如果 root 調(diào)用 cancel 函數(shù)那么所有 children 也都會級聯(lián) cancel, 因為保存 children 的是一個 map, 也就無所謂先序中序后序了。如果 ctx 1-1 cancel, 那么他的 children 都會 cancel, 但是 rootctx 1-2 則不會受影響。

        業(yè)務(wù)代碼當(dāng)調(diào)用棧比較深時,就會出現(xiàn)這個多叉樹的形狀,另外 http 庫己經(jīng)集成了 context, 每個 endpoint 的請求自帶一個從 http 庫派生出來的 child

        func (c *cancelCtx) cancel(removeFromParent bool, err error) {
         if err == nil {
          panic("context: internal error: missing cancel error")
         }
         c.mu.Lock()
         if c.err != nil {
          c.mu.Unlock()
          return // already canceled
         }
         c.err = err
         if c.done == nil {
          c.done = closedchan
         } else {
          close(c.done)
         }
         for child := range c.children {
          // NOTE: acquiring the child's lock while holding parent's lock.
          child.cancel(false, err)
         }
         c.children = nil
         c.mu.Unlock()

         if removeFromParent {
          removeChild(c.Context, c)
         }
        }

        可以通過 cancelCtxcancel 看到原理,級聯(lián) cancel 所有 children

        場景

        來看一下使用場景吧,以一個標(biāo)準(zhǔn)的 watch etcd 來入手

        func watch(ctx context.Context, revision int64) {
         ctx, cancel := context.WithCancel(ctx)
         defer cancel()

         for {
          rch := watcher.Watch(ctx, watchPath, clientv3.WithRev(revision))
          for wresp := range rch {
            ......
              doSomething()
          }

          select {
          case <-ctx.Done():
           // server closed, return
           return
          default:
          }
         }
        }

        首先基于參數(shù)傳進來的 parent ctx 生成了 child ctxcancel 函數(shù)。然后 Watch 時傳入 child ctx, 如果此時 parent ctx 被外層 cancel 的話,child ctx 也會被 cancel, rch 會被 etcd clientv3 關(guān)閉,然后 for 循環(huán)走到 select 邏輯,此時 child ctx 被取消了,所以 <-ctx.Done() 生效,watch 函數(shù)返回。

        其于 context 可以很好的做到多個 goroutine 協(xié)作,超時管理,大大簡化了開發(fā)工作。

        Bad Cases

        那我們看幾個錯誤使用 context 的案例,都非常經(jīng)典

        1. 打印 ctx

        func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
         c := newCancelCtx(parent)
         propagateCancel(parent, &c)
         return &c, func() { c.cancel(true, Canceled) }
        }

        // newCancelCtx returns an initialized cancelCtx.
        func newCancelCtx(parent Context) cancelCtx {
         return cancelCtx{Context: parent}
        }

        WithCancel 為例子,可以看到 child 同時引用了 parent, 而 propagateCancel 函數(shù)的存在,parent 也會引用 child(當(dāng) parent 是 cancelCtx 類型時).

        如果此時打印 ctx, 就會遞歸調(diào)用 String() 方法,就會把 key/value 打印出來。如果此時 value 是非線程安全的,比如 map, 就會引發(fā) concurrent read and write panic.

        這個案例就是 http 標(biāo)準(zhǔn)庫的實現(xiàn) server.go:2906[2] 行代碼,把 http server 保存到 ctx 中

        ctx := context.WithValue(baseCtx, ServerContextKey, srv)

        最后調(diào)用業(yè)務(wù)層代碼時把 ctx 傳給了用戶

        go c.serve(connCtx)

        如果此時打印 ctx, 就會打印 http srv 結(jié)構(gòu)體,這里面就有 map. 感興趣的可以做個實驗,拿 ab 壓測很容易復(fù)現(xiàn)。

        2. 提前超時

        func test(){
         ctx, cancel := context.WithCancel(ctx)
         defer cancel()
          
          doSomething(ctx)
        }

        func doSomething(ctx){
          go doOthers(ctx)
        }

        當(dāng)調(diào)用棧較深,多人合作時很容易產(chǎn)生這種情況。其實還是沒明白 ctx cancel 工作原理,異步 go 出去的業(yè)務(wù)邏輯需要基于 context.Background() 再派生 child ctx, 否則就會提前超時返回

        3. 自定義 ctx

        理論上沒必要自定義 ctx, 相比官方實現(xiàn),自定義有個很大的開銷在于 child 如何響應(yīng) parent cancel

        // propagateCancel arranges for child to be canceled when parent is.
        func propagateCancel(parent Context, child canceler) {
          ......
         if p, ok := parentCancelCtx(parent); ok {
          p.mu.Lock()
          if p.err != nil {
          ......
          } else {
          ......
           p.children[child] = struct{}{}
          }
          p.mu.Unlock()
         } else {
          atomic.AddInt32(&goroutines, +1)
          go func() {
           select {
           case <-parent.Done():
            child.cancel(false, parent.Err())
           case <-child.Done():
           }
          }()
         }
        }

        func parentCancelCtx(parent Context) (*cancelCtx, bool) {
          ......
         p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
         if !ok {
          return nilfalse
         }
          ......
         return p, true
        }

        通過源碼可知,parent 引用 child 有兩種方式,官方 cancelCtx 類型的是用 map 保存。但是非官方的需要開啟 goroutine 去監(jiān)測。本來業(yè)務(wù)代碼己經(jīng) goroutine 滿天飛了,不加節(jié)制的使用只會增加系統(tǒng)負擔(dān)。

        另外聽說某大公司嫌棄這個 map, 想要使用數(shù)組重寫一版:(

        原則

        最后來總結(jié)下 context 使用的幾個原則:

        1. 除了框架層不要使用 WithValue 攜帶業(yè)務(wù)數(shù)據(jù),這個類型是 interface{}, 編譯期無法確定,運行時 assert 有開銷。如果真要攜帶也要用 thread-safe 的數(shù)據(jù)
        2. 一定不要打印 context, 尤其是從 http 標(biāo)準(zhǔn)庫派生出來的,誰知道里面存了什么
        3. context 做為第一個參數(shù)傳給函數(shù),而不是當(dāng)成結(jié)構(gòu)體的成員字段來使用(雖然 etcd 代碼也這么用)
        4. 盡可能不要自定義用戶層 context,除非收益巨大
        5. 異步 goroutine 邏輯使用 context 時要清楚誰還持有,會不會提前超時
        6. 派生出來的 child ctx 一定要配合 defer cancel() 使用,釋放資源

        小結(jié)

        這次分享就這些,以后面還會分享更多的內(nèi)容,如果感興趣,可以關(guān)注并轉(zhuǎn)發(fā)(:

        參考資料

        [1]

        Context should go away for Go 2: https://faiface.github.io/post/context-should-go-away-go2/,

        [2]

        server.go: https://github.com/golang/go/blob/master/src/net/http/server.go#L2878,



        推薦閱讀


        福利

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

        瀏覽 123
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产裸身美女网站 | 成年人免费看黄 | 国产精品 久久久精品cos | 欧美+国产+无码+麻豆 | 国产偷拍自拍在线视频 | 草在线观看免费视频 | 特级西西人体444WWwtini | 蜜乳av中文 | 欧美日韩三级在线 | 精品人妻一区二区三区含羞草 |