golang -context

1. 簡(jiǎn)介

go 1.7 開始引入context(上下文),準(zhǔn)確地說是goroutine 的上下文。主要在goroutine 間傳遞上下文消息,包括了取消信號(hào), 超時(shí)時(shí)間,截止時(shí)間和k-v 等。

在web程序中,每個(gè)Request都需要開啟一個(gè)goroutine做一些事情,這些goroutine又可能會(huì)開啟其他的 goroutine去訪問后端資源,比如數(shù)據(jù)庫、RPC服務(wù)等,它們需要訪問一些共享的資源,比如用戶身份信息、認(rèn)證token、請(qǐng)求截止時(shí)間等 這時(shí)候可以通過Context,來跟蹤這些goroutine,并且通過Context來控制它們, 這就是Go語言為我們提供的Context,中文可以稱之為“上下文”。

2. 接口定義

主要包括了如下兩個(gè)接口定義

  • Context
    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    1. Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回值是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context會(huì)自動(dòng)發(fā)起取消請(qǐng)求; 第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。
    2. Done方法返回一個(gè)只讀的chan,類型為struct{},在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請(qǐng)求, 我們通過Done方法收到這個(gè)信號(hào)后,就應(yīng)該做清理操作,然后退出goroutine,釋放資源。之后,Err 方法會(huì)返回一個(gè)錯(cuò)誤,告知為什么 Context 被取消
    3. Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消,例如 被取消或者超時(shí)
    4. Value方法獲取該Context上綁定的值,是一個(gè)鍵值對(duì),通過一個(gè)Key才可以獲取對(duì)應(yīng)的值
  • canceler

    type canceler interface {
      cancel(removeFromParent bool, err error)
      Done() <-chan struct{}
     }
    

    實(shí)現(xiàn)了canceler 接口的context說明是可取消的,在context 包中有兩個(gè)類型實(shí)現(xiàn)了該接口,分別是:*cancelCtx*timerCtx

3. 接口實(shí)現(xiàn)類型

官方提供了如下四種的 context 類型,分別是 emptyCtx,cancelCtx,timerCtx,valueCtx。

3.1 emptyCtx

實(shí)現(xiàn)了Context接口,但都是直接 return 默認(rèn)值,沒有具體功能代碼, 底層類型是int, 同時(shí)實(shí)現(xiàn)了Stringer接口(String方法)

type emptyCtx int

func (*emptyCtx) Done() <-chan struct{} {
  return nil
}

func (*emptyCtx) Err() error {
  return nil
}

func (*emptyCtx) Value(key any) any {
  return nil
}

func (e *emptyCtx) String() string {
  switch e {
  case background:
      return "context.Background"
  case todo:
      return "context.TODO"
  }
  return "unknown empty Context"
}

emptyCtx一般用作最初始的 context,作為父 context 使用, 官方定義了兩個(gè)可導(dǎo)出的empptCty,通過函數(shù)Background()TODO()返回

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)


func Background() Context {
    return background
}


func TODO() Context {
    return todo
}

3.2 cancelCtx

實(shí)現(xiàn)了Contextcanceler 接口, 結(jié)構(gòu)體定義如下

type cancelCtx struct {
  Context

  mu       sync.Mutex            // protects following fields
  done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
  children map[canceler]struct{} // set to nil by the first cancel call
  err      error                 // set to non-nil by the first cancel call
}
  • 接口實(shí)現(xiàn)

    cancelCtx組合了匿名Context 字段,所以也是一個(gè)Context, 同時(shí)它又實(shí)現(xiàn)了canceler 接口,
    其中對(duì)于Context, 重寫了 Done() ,Value()Err() 方法,其他的繼承父類的
    具體代碼如下:

    //  `Done()` 方法 采用`懶漢式`方式, 當(dāng)調(diào)用該方法時(shí),`c.done`才會(huì)存儲(chǔ)一個(gè)新創(chuàng)建的一個(gè)只讀  `channel d`,并返回
    func (c *cancelCtx) Done() <-chan struct{} {
        d := c.done.Load()
        if d != nil {
            return d.(chan struct{})
        }
        c.mu.Lock()
        defer c.mu.Unlock()
        d = c.done.Load()
        if d == nil {
            d = make(chan struct{})
            c.done.Store(d)
        }
        return d.(chan struct{})
    }   
    
    func (c *cancelCtx) Value(key any) any {
        if key == &cancelCtxKey {
            return c
        }
        return value(c.Context, key)
    }
    
    func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &cancelCtxKey {
                return c
            }
            c = ctx.Context
        case *timerCtx:
            if key == &cancelCtxKey {
                return &ctx.cancelCtx
            }
            c = ctx.Context
        case *emptyCtx:
            return nil
        default:
            return c.Value(key)
        }
      }
    }
    
    func (c *cancelCtx) Err() error {
        c.mu.Lock()
        err := c.err
        c.mu.Unlock()
        return err
    }
    

    重點(diǎn)講一下重寫的 func (c *cancelCtx) Value(key any) any方法, 如果傳入的key 是cancelCtxKey 就會(huì)直接返回當(dāng)前的cancelCtx, 反之就會(huì)通過包內(nèi)置的value()函數(shù),逐層向上父節(jié)點(diǎn)傳遞,知道找到對(duì)應(yīng)的key 或者找不到

    對(duì)于canceler具體實(shí)現(xiàn)的cancel()方法如下, Done() 同上:

    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
        // 必須傳入err,即導(dǎo)致ctx cancel 的error
        if err == nil {
            panic("context: internal error: missing     cancel error")
        }
        c.mu.Lock()
            // 如果ctx 中已經(jīng)有err了,說明該ctx 已經(jīng)被      cancel了
        if c.err != nil {
            c.mu.Unlock()
            return // already canceled
        }
           // 關(guān)閉done 中的channel d
        c.err = err
        d, _ := c.done.Load().(chan struct{})
        if d == nil {
            c.done.Store(closedchan)
        } else {
            close(d)
        }
            // 遞歸遍歷子節(jié)點(diǎn)
        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()
            // 是否從父節(jié)點(diǎn)中刪除
        if removeFromParent {
            removeChild(c.Context, c)
        }
     }
    

    簡(jiǎn)單的來說cancel 方法就是關(guān)閉c.done 中的channel,遞歸取消子節(jié)點(diǎn)。
    其中removeChild 函數(shù)就是將子節(jié)點(diǎn)從父節(jié)點(diǎn)中的children map 中刪除, 代碼如下:

    func removeChild(parent Context, child canceler) {
      p, ok := parentCancelCtx(parent)
      if !ok {
        return
      }
        p.mu.Lock()
      if p.children != nil {
        delete(p.children, child)
      }
      p.mu.Unlock()
    }
    // 向上遍歷找到可取消的父節(jié)點(diǎn),因?yàn)楫?dāng)前的父節(jié)點(diǎn)可能不是可取消的類型, 即不是*cancelCtx 和 *timerCtx 類型
    func parentCancelCtx(parent Context) (*cancelCtx, bool) {
        done := parent.Done()
          // 父節(jié)點(diǎn)已經(jīng)取消或者不存在
        if done == closedchan || done == nil {
            return nil, false
        }  
            // 向上找到可取消的父節(jié)點(diǎn)并返回,*cancelCtx 或者*timerCtx 中的*cancelCtx
        p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
        if !ok {
            return nil, false
        }
        pdone, _ := p.done.Load().(chan struct{})
        if pdone != done {
            return nil, false
        }
        return p, true
      }
    
  • 創(chuàng)建cancelCtc函數(shù):

    通過如下WithCancel函數(shù)創(chuàng)建, 傳入一個(gè)父節(jié)點(diǎn),返回子節(jié)點(diǎn)和對(duì)應(yīng)的取消函數(shù)

    func WithCancel(parent Context) (ctx Context,   cancel CancelFunc) {
        if parent == nil {
          panic("cannot create context from nil   parent")
      }
      c := newCancelCtx(parent)
      propagateCancel(parent, &c)
      return &c, func() { c.cancel(true, Canceled) }
    }
    

    其中propagateCancel 函數(shù)主要是向上遍歷可cancel的父節(jié)點(diǎn), 并掛靠在該父節(jié)點(diǎn)上

    func propagateCancel(parent Context, child canceler) {
           // 父節(jié)點(diǎn)是不可取消的類型即 emptyCtx  或者是valueCtx
        done := parent.Done()
        if done == nil {
          return // parent is never canceled
        }
            // 監(jiān)聽下父節(jié)點(diǎn)的Done() channel 判斷是否已經(jīng)取消,是的會(huì)此時(shí)直接取消當(dāng)前子節(jié)點(diǎn)
        select {
        case <-done:
          // parent is already canceled
          child.cancel(false, parent.Err())
          return
        default:
        }
            // 向上找到可取消的父節(jié)點(diǎn)
        if p, ok := parentCancelCtx(parent); ok {
          p.mu.Lock()
                  // 父節(jié)點(diǎn)已經(jīng)取消
          if p.err != nil {
              // parent has already been canceled
              child.cancel(false, p.err)
          } else {
              if p.children == nil {
                  p.children = make(map[canceler]struct{})
              }
              p.children[child] = struct{}{}
          }
            p.mu.Unlock()
        } else {
            atomic.AddInt32(&goroutines, +1)
             // parentCancelCtx 函數(shù)只能找到cancelCtx 或者timerCtx 類型的父類,下面是監(jiān)聽其他類型的可取消的父類,當(dāng)父類取消,則關(guān)聯(lián)取消子類
             go func() {
              select {
              case <-parent.Done():
                  child.cancel(false, parent.Err())
              // 該case 防止父節(jié)點(diǎn)未取消,而當(dāng)前節(jié)點(diǎn)已經(jīng)取消導(dǎo)致的goroutine 泄漏
              case <-child.Done():
              }
          }()
      }
    }
    
    

3.3 timerCtx

timerCtx 其實(shí)繼承類cancelCtx 并擴(kuò)展了功能, 結(jié)構(gòu)體定義如下:

type timerCtx struct {
  cancelCtx
  timer *time.Timer 
  deadline time.Time
}
  • 接口實(shí)現(xiàn)
    timerCtx 重寫了DeadLinecancel 方法: 具體如下
func (c *timerCtx) Deadline() (deadline time.Time,   ok bool) {
    return c.deadline, true
}


func (c *timerCtx) cancel(removeFromParent bool, err error) {
      c.cancelCtx.cancel(false, err)
      if removeFromParent {
        // Remove this timerCtx from its parent   cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
      c.timer.Stop()
      c.timer = nil
    }
    c.mu.Unlock()
}
  • 創(chuàng)建timerCtc函數(shù):
    // 創(chuàng)建超時(shí)時(shí)間段的timerCtx
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
        return WithDeadline(parent, time.Now().Add(timeout))
    }
    
      // 創(chuàng)建截止時(shí)間的timerCtx
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
        if parent == nil {
          panic("cannot create context from nil parent")
        }
        // 如果當(dāng)前的截止時(shí)間早于父節(jié)點(diǎn)的截止時(shí)間, 則直接返貨cancelCtx 類型的context, 因?yàn)橐欢ㄊ歉腹?jié)點(diǎn)早于該節(jié)點(diǎn)取消,或者是該節(jié)點(diǎn)主動(dòng)調(diào)用子節(jié)點(diǎn)
        if cur, ok := parent.Deadline(); ok && cur.Before(d) {
          // The current deadline is already sooner than the new one.
          return WithCancel(parent)
      }
        c := &timerCtx{
            cancelCtx: newCancelCtx(parent),
            deadline:  d,
        }
        propagateCancel(parent, c)
        dur := time.Until(d)
        if dur <= 0 {
            c.cancel(true, DeadlineExceeded) // deadline has already passed
            return c, func() { c.cancel(false, Canceled) }
        }
      c.mu.Lock()
      defer c.mu.Unlock()
          // 開起計(jì)時(shí)器,到時(shí)間自動(dòng)執(zhí)行取消
      if c.err == nil {
          c.timer = time.AfterFunc(dur, func() {
              c.cancel(true, DeadlineExceeded)
          })
        }
        return c, func() { c.cancel(true, Canceled) }
    }
    
    

3.4 valueCtx

用來傳遞K-V鍵值對(duì)的context, 底層結(jié)構(gòu)體定義如下:

type valueCtx struct {
  // 通過組合匿名字段,繼承了Context, 在這邊可以理解為上層父節(jié)點(diǎn)
  Context
  // 鍵值對(duì)數(shù)據(jù)
  key, val any
}

valueCtx 重寫了Value方法, 即向上遍歷父節(jié)點(diǎn)的key ,直到找到對(duì)應(yīng)的key 或者找不到

 func (c *valueCtx) Value(key any) any {
   if c.key == key {
       return c.val
   }
   return value(c.Context, key)
 }

valueCtx的通過如下WithValue創(chuàng)建:

func WithValue(parent Context, key, val any) Context {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

注意:
key 不能為nil, 同時(shí)key的類型必須是可比較類型, 函數(shù)返回的是*valueCtx 類型

4. 注意事項(xiàng)

官方提出了如下的幾個(gè)注意事項(xiàng)

  1. 不要將context 放在結(jié)構(gòu)體里,直接將context 作為函數(shù)的第一個(gè)參數(shù)傳入,而且一般命名為ctx
  2. 不要向函數(shù)傳入nil 屬性的context, 如果不知道傳什么就傳context.TODO()
  3. 不要將應(yīng)該作為函數(shù)參數(shù)的類型塞到ctx中,ctx 應(yīng)該存儲(chǔ)的是一些共有的數(shù)據(jù),例如登錄的token 或者cookie等
  4. 同一個(gè)context 是并發(fā)安全的,可以傳遞到多個(gè)goroutine 中

5. 使用實(shí)例代碼

// cancelCtx
gen := func(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // returning not to leak the goroutine
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
    fmt.Println(n)
    if n == 5 {
        break
    }
}


// timerCtx
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()



----------------------------------
select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}


// valueCtx
type favContextKey string

f := func(ctx context.Context, k favContextKey) {
    if v := ctx.Value(k); v != nil {
        fmt.Println("found value:", v)
        return
    }
    fmt.Println("key not found:", k)
}

k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")

f(ctx, k)
f(ctx, favContextKey("color"))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 為什么需要context 在并發(fā)程序中,由于超時(shí)、取消操作或者一些異常情況,往往需要進(jìn)行搶占操作或者中斷后續(xù)操作。...
    陳二狗想吃肉閱讀 743評(píng)論 0 2
  • [TOC] Golang Context分析 Context背景 和 適用場(chǎng)景 golang在1.6.2的時(shí)候還沒...
    AllenWu閱讀 11,630評(píng)論 0 30
  • 參考Go語言實(shí)戰(zhàn)筆記(二十)| Go ContextGolang context初探 一、WaitGroup 這是...
    合肥黑閱讀 623評(píng)論 0 10
  • overview Package context defines the Context type, which ...
    wncbbnk閱讀 641評(píng)論 0 0
  • context包專門用來簡(jiǎn)化處理單個(gè)請(qǐng)求的多個(gè)goroutine之間與請(qǐng)求域的數(shù)據(jù)、取消信號(hào)、截止時(shí)間等相關(guān)操作。...
    wz998閱讀 3,950評(píng)論 0 3

友情鏈接更多精彩內(nèi)容