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

          也許是 Go Context 最佳實(shí)踐

          共 6814字,需瀏覽 14分鐘

           ·

          2021-06-13 00:39

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

          但是我發(fā)現(xiàn)并不是每個(gè)人都了解 context, 從去年到現(xiàn)在就見(jiàn)過(guò)兩次因?yàn)殄e(cuò)誤使用導(dǎo)致的問(wèn)題。每個(gè)同學(xué)都會(huì)踩到坑,今天分享下 context 庫(kù)使用的 Dos and Don'ts

          原理

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

          Context 是一個(gè)接口

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

          目前的實(shí)現(xiàn)有 emptyCtx, valueCtx, cancelCtx, timerCtx. 可以基于某個(gè) 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)

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

          context tree

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

          業(yè)務(wù)代碼當(dāng)調(diào)用棧比較深時(shí),就會(huì)出現(xiàn)這個(gè)多叉樹(shù)的形狀,另外 http 庫(kù)己經(jīng)集成了 context, 每個(gè) endpoint 的請(qǐng)求自帶一個(gè)從 http 庫(kù)派生出來(lái)的 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)
           }
          }

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

          場(chǎng)景

          來(lái)看一下使用場(chǎng)景吧,以一個(gè)標(biāo)準(zhǔn)的 watch etcd 來(lái)入手

          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ù)傳進(jìn)來(lái)的 parent ctx 生成了 child ctxcancel 函數(shù)。然后 Watch 時(shí)傳入 child ctx, 如果此時(shí) parent ctx 被外層 cancel 的話(huà),child ctx 也會(huì)被 cancel, rch 會(huì)被 etcd clientv3 關(guān)閉,然后 for 循環(huán)走到 select 邏輯,此時(shí) child ctx 被取消了,所以 <-ctx.Done() 生效,watch 函數(shù)返回。

          其于 context 可以很好的做到多個(gè) goroutine 協(xié)作,超時(shí)管理,大大簡(jiǎn)化了開(kāi)發(fā)工作。

          Bad Cases

          那我們看幾個(gè)錯(cuò)誤使用 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 同時(shí)引用了 parent, 而 propagateCancel 函數(shù)的存在,parent 也會(huì)引用 child(當(dāng) parent 是 cancelCtx 類(lèi)型時(shí)).

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

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

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

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

          go c.serve(connCtx)

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

          2. 提前超時(shí)

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

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

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

          3. 自定義 ctx

          理論上沒(méi)必要自定義 ctx, 相比官方實(shí)現(xiàn),自定義有個(gè)很大的開(kāi)銷(xiāo)在于 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
          }

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

          另外聽(tīng)說(shuō)某大公司嫌棄這個(gè) map, 想要使用數(shù)組重寫(xiě)一版:(

          原則

          最后來(lái)總結(jié)下 context 使用的幾個(gè)原則:

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

          小結(jié)

          這次分享就這些,以后面還會(huì)分享更多的內(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,



          推薦閱讀


          福利

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

          瀏覽 123
          點(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一级黄色视频 | 最新国产毛片 | 波多野结衣久久精品 | 午夜精品偷拍 | 婷婷丁香激情五月天 |