首頁 > 軟體

對Go語言中的context包原始碼分析

2022-02-09 13:01:45

一、包說明分析

context包:這個包分析的是1.15

context包定義了一個Context型別(介面型別),通過這個Context介面型別, 就可以跨api邊界/跨程序傳遞一些deadline/cancel訊號/request-scoped值.

發給server的請求中需要包含Context,server需要接收Context. 在整個函數呼叫鏈中,Context都需要進行傳播. 期間是可以選擇將Context替換為派生Context(由With-系列函數生成). 當一個Context是canceled狀態時,所有派生的Context都是canceled狀態.

With-系列函數(不包含WithValue)會基於父Context來生成一個派生Context, 還有一個CancelFunc函數,呼叫這個CancelFun函數可取消派生物件和 "派生物件的派生物件的...",並且會刪除父Context和派生Context的參照關係, 最後還會停止相關定時器.如果不呼叫CancelFunc,直到父Context被取消或 定時器觸發,派生Context和"派生Context的派生Context..."才會被回收, 否則就是洩露leak. go vet工具可以檢測到洩露.

使用Context包的程式需要遵循以下以下規則,目的是保持跨包相容, 已經使用靜態分析工具來檢查context的傳播:

  • Context不要儲存在struct內,直接在每個函數中顯示使用,作為第一個引數,名叫ctx
  • 即使函數允許,也不要傳遞nil Context,如果實在不去確定就傳context.TODO
  • 在跨程序和跨api時,要傳request-scoped資料時用context Value,不要傳函數的可選引數
  • 不同協程可以傳遞同一Context到函數,多協程並行使用Context是安全的

二、包結構分析

核心的是:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    type CancelFunc
    type Context

從上可以看出,核心的是Context介面型別,圍繞這個型別出現了With-系列函數, 針對派生Context,還有取消函數CancelFunc.

還有兩個暴露的變數:

Canceled

context取消時由Context.Err方法返回

DeadlineExceeded

context超過deadline時由Context.Err方法返回

三、Context介面型別分析

context也稱上下文.

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

先看說明:

跨api時,Context可以攜帶一個deadline/一個取消訊號/某些值. 並行安全.

方法集分析:

Deadline

  • 返回的是截至時間
  • 這個時間表示的是任務完成時間
  • 到這個時間點,Context的狀態已經是be canceled(完成狀態)
  • ok為false表示沒有設定deadline
  • 連續呼叫,返回的結果是相同的

Done

  • 返回的唯讀通道
  • 任務完成,通道會被關閉,Context狀態是be canceled
  • Conetext永遠不be canceled,Done可能返回nil
  • 連續呼叫,返回的結果是相同的
  • 通道的關閉會非同步發生,且會在取消函數CancelFunc執行完之後發生
  • 使用方面,Done需要配合select使用
  • 更多使用Done的例子在這個部落格

Err

  • Done還沒關閉(此處指Done返回的唯讀通道),Err返回nil
  • Done關閉了,Err返回non-nil的error
  • Context是be canceled,Err返回Canceled(這是之前分析的一個變數)
  • 如果是超過了截至日期deadline,Err返回DeadlineExceeded
  • 如果Err返回non-nil的error,後續再次呼叫,返回的結果是相同的

Value

  • 引數和返回值都是interface{}型別(這種解耦方式值得學習)
  • Value就是通過key找value,如果沒找到,返回nil
  • 連續呼叫,返回的結果是相同的
  • 上下文值,只適用於跨程序/跨api的request-scoped資料
  • 不適用於代替函數可選項
  • 一個上下文中,一個key對應一個value
  • 典型用法:申請一個全域性變數來放key,在context.WithValue/Context.Value中使用
  • key應該定義為非暴露型別,避免衝突
  • 定義key時,應該支援型別安全的存取value(通過key)
  • key不應該暴露
    • 表示應該通過暴露函數來進行隔離(具體可以檢視原始碼中的例子)

四、後續分析規劃

看完Context的介面定義後,還需要檢視With-系列函數才能知道context的定位, 在With-系列中會涉及到Context的使用和內部實現,那就先看WithCancel.

withCancel:

  • CancelFunc
  • newCancelCtx
    • cancelCtx
  • canceler
  • propagateCancel
    • parentCancelCtx

以下是分析出的通過規則:很多包對外暴露的是介面型別和幾個針對此型別的常用函數. 介面型別暴露意味可延伸,但是想擴充套件之後繼續使用常用函數,那擴充套件部分就不能 修改常用函數涉及的部分,當然也可以通過額外的介面繼續解耦. 針對"暴露介面和常用函數"這種套路,實現時會存在一個非暴露的實現型別, 常用函數就是基於這個實現型別實現的.在context.go中的實現型別是emptyCtx. 如果同時需要擴充套件介面和常用函數,最好是重新寫一個新包.

下面的分析分成兩部分:基於實現型別到常用函數;擴充套件功能以及如何擴充套件.

五、基於實現型別到常用函數

Context介面的實現型別是emptyCtx. 

  type emptyCtx int
    func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return }
    func (*emptyCtx) Done() <-chan struct{} { return nil }
    func (*emptyCtx) Err() error { return nil }
    func (*emptyCtx) Value(key interface{}) interface{} { 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介面,還實現了context.stringer介面, 注意下面的String不是實現的fmt.Stringer介面,而是未暴露的context.stringer介面. 正如empty的名字,對Context介面的實現都是空的,後續需要針對emptyCtx做擴充套件.

    var (
      background = new(emptyCtx)
      todo       = new(emptyCtx)
    )
    func Background() Context {
      return background
    }
    func TODO() Context {
      return todo
    }

這裡通過兩個暴露的函數建立兩個空的emptyCtx範例,後續會根據不同場景來擴充套件. 在註釋中,background範例的使用場景是:main函數/初始化/測試/或者作為top-level 的Context(派生其他Context);todo範例的使用場景是:不確定時用todo. 到此emptyCtx的構造就理順了,就是Background()/TODO()兩個函數,之後是針對她們 的擴充套件和Context派生.

Context派生是基於With-系列函數實現的,我們先看對emptyCtx的擴充套件, 這些擴充套件至少會覆蓋一部分函數,讓空的上下文變成支援某些功能的上下文, 取消訊號/截至日期/值,3種功能的任意組合.

從原始碼中可以看出,除了emptyCtx,還有cancelCtx/myCtx/myDoneCtx/otherContext/ timeCtx/valueCtx,他們有個共同特點:基於Context組合的新型別, 我們尋找的對emptyCtx的擴充套件,就是在這些新型別的方法中.

小技巧:emptyCtx已經實現了context.Context,如果要修改方法的實現, 唯一的方法就是利用Go的內嵌進行方法的覆蓋.簡單點說就是內嵌到struct, struct再定義同樣簽名的方法,如果不需要資料,內嵌到介面也是一樣的.

cancelCtx

支援取消訊號的上下文

type cancelCtx struct {
  Context

  mu       sync.Mutex
  done     chan struct{}
  children map[canceler]struct{}
  err      error
}

看下方法:

  var cancelCtxKey int
    func (c *cancelCtx) Value(key interface{}) interface{} {
      if key == &cancelCtxKey {
        return c
      }
      return c.Context.Value(key)
    }
    func (c *cancelCtx) Done() <-chan struct{} {
      c.mu.Lock()
      if c.done == nil {
        c.done = make(chan struct{})
      }
      d := c.done
      c.mu.Unlock()
      return d
    }
    func (c *cancelCtx) Err() error {
      c.mu.Lock()
      err := c.err
      c.mu.Unlock()
      return err
    }

cancelCtxKey預設是0,Value()要麼返回自己,要麼呼叫上下文Context.Value(), 具體使用後面再分析;Done()返回cancelCtx.done;Err()返回cancelCtx.err;

    func contextName(c Context) string {
      if s, ok := c.(stringer); ok {
        return s.String()
      }
      return reflectlite.TypeOf(c).String()
    }
    func (c *cancelCtx) String() string {
      return contextName(c.Context) + ".WithCancel"
    }

internal/reflectlite.TypeOf是獲取介面動態型別的反射型別, 如果介面是nil就返回nil,此處是獲取Context的型別, 從上面的分析可知,頂層Context要麼是background,要麼是todo, cancelCtx實現的context.stringer要麼是context.Background.WithCancel, 要麼是context.TODO.WithCancel.這裡說的只是頂層Context下的, 多層派生Context的結構也是類似的.

值得注意的是String()不屬於Context介面的方法集,而是emptyCtx對 context.stringer介面的實現,cancelCxt內嵌的Context,所以不會覆蓋 emptyCtx對String()的實現. 

   var closedchan = make(chan struct{})
    func init() {
      close(closedchan)
    }

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

cancel(),具體的取消信令對應的操作,err不能為nil,err會存到cancelCtx.err, 如果已經存了,表示取消操作已經執行.關閉done通道,如果之前沒有呼叫Done() 來獲取done通道,就返回一個closedchan(這是要給已關閉通道,可重用的), 之後是呼叫children的cancel(),最後就是在Context樹上移除當前派生Context.

    func parentCancelCtx(parent Context) (*cancelCtx, bool) {
      done := parent.Done()
      if done == closedchan || done == nil {
        return nil, false
      }
      p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
      if !ok {
        return nil, false
      }
      p.mu.Lock()
      ok = p.done == done
      p.mu.Unlock()
      if !ok {
        return nil, false
      }
      return p, true
    }
    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()
    }

removeChild首先判斷父Context是不是cancelCtx型別, 再判斷done通道和當前Context的done通道是不是一致的, (如果不一致,說明:done通道是diy實現的,就不能刪掉了).

到此,cancelCtx覆蓋了cancelCtx.Context的Done/Err/Value, 同時實現了自己的列印函數String(),還實現了cancel(). 也就是說cancelCtx還實現了介面canceler:

type canceler interface {
  cancel(removeFromParent bool, err error)
  Done() <-chan struct{}
}
// cancelCtx.children的定義如下:
// children map[canceler]struct{}

執行取消訊號對應的操作時,其中有一步就是執行children的cancel(), children的key是canceler介面型別,所以有對cancel()的實現. cancelCtx實現了canceler介面,那麼在派生Context就可以巢狀很多層, 或派生很多個cancelCtx.

func newCancelCtx(parent Context) cancelCtx {
  return cancelCtx{Context: parent}
}

非暴露的建構函式.

回顧一下:cancelCtx新增了Context對取消訊號的支援. 只要觸發了"取消訊號",使用方只需要監聽done通道即可.

myCtx myDoneCtx otherContext屬於測試,等分析測試的時候再細說.

timerCtx

前面說到了取消訊號對應的上下文cancelCtx,timerCtx就是基於取消訊號上下擴充套件的

type timerCtx struct {
  cancelCtx
  timer *time.Timer

  deadline time.Time
}

註釋說明:內嵌cancelCtx是為了複用Done和Err,擴充套件了一個定時器和一個截至時間, 在定時器觸發時觸發cancelCtx.cancel()即可.

  func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
      return c.deadline, true
    }
    func (c *timerCtx) String() string {
      return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
        c.deadline.String() + " [" +
        time.Until(c.deadline).String() + "])"
    }
    func (c *timerCtx) cancel(removeFromParent bool, err error) {
      c.cancelCtx.cancel(false, err)
      if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
      }
      c.mu.Lock()
      if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
      }
      c.mu.Unlock()
    }

timerCtx內嵌了cancelCtx,說明timerCtx也實現了canceler介面, 從原始碼中可以看出,cancel()是重新實現了,String/Deadline都重新實現了.

cancel()中額外新增了定時器的停止操作.

這裡沒有deadline設定和定時器timer開啟的操作,會放在With-系列函數中.

回顧一下: Context的deadline是機會取消訊號實現的.

valueCtx

valueCtx和timerCtx不同,是直接基於Context的.

type valueCtx struct {
  Context
  key, val interface{}
}

一個valueCtx附加了一個kv對.實現了ValueString.

   func stringify(v interface{}) string {
      switch s := v.(type) {
      case stringer:
        return s.String()
      case string:
        return s
      }
      return "<not Stringer>"
    }
    func (c *valueCtx) String() string {
      return contextName(c.Context) + ".WithValue(type " +
        reflectlite.TypeOf(c.key).String() +
        ", val " + stringify(c.val) + ")"
    }
    func (c *valueCtx) Value(key interface{}) interface{} {
      if c.key == key {
        return c.val
      }
      return c.Context.Value(key)
    }

因為valueCtx.val型別是介面型別interface{},所以獲取具體值時, 使用了switch type.

六、With-系列函數

支援取消訊號 WithCancel:

var Canceled = errors.New("context canceled")
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) }
}

派生一個支援取消訊號的Context,型別是cancelCtx,CancelFunc是取消操作, 具體是呼叫cancelCtx.cancel()函數,err引數是Canceled.

    func propagateCancel(parent Context, child canceler) {
      done := parent.Done()
      if done == nil {
        return // parent is never canceled
      }

      select {
      case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
      default:
      }

      if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        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)
        go func() {
          select {
          case <-parent.Done():
            child.cancel(false, parent.Err())
          case <-child.Done():
          }
        }()
      }
    }

傳播取消訊號.如果父Context不支援取消訊號,那就不傳播. 如果父Context的取消訊號已經觸發(就是父Context的done通道已經觸發或關閉), 之後判斷父Context是不是cancelCtx,如果是就將此Context丟到children中, 如果父Context不是cancelCtx,那就起協程監聽父子Context的done通道.

小技巧:

select {
case <-done:
  child.cancel(false, parent.Err())
  return
default:
}

不加default,會等到done通道有動作;加了會立馬判斷done通道,done沒操作就結束select.

select {
case <-parent.Done():
  child.cancel(false, parent.Err())
case <-child.Done():
}

這個會等待,因為沒有加default.

因為頂層Context目前只能是background和todo,不是cancelCtx, 所以頂層Context的直接派生Context不會觸發propagateCancel中的和children相關操作, 至少得3代及以後才有可能.

WithCancel的取消操作會釋放相關資源,所以在上下文操作完之後,最好儘快觸發取消操作. 觸發的方式是:done通道觸發,要麼有資料,要麼被關閉.

支援截至日期 WithDeadline:

 

   func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
      if parent == nil {
        panic("cannot create context from nil parent")
      }
      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()
      if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
          c.cancel(true, DeadlineExceeded)
        })
      }
      return c, func() { c.cancel(true, Canceled) }
    }

With-系列函數用於生成派生Context,函數內部第一步都是判斷父Context是否為nil, WithDeadline第二步是判斷父Context是否支援deadline,支援就將取消訊號傳遞給派生Context, 如果不支援,就為當前派生Context支援deadline.

先理一下思路,目前Context的實現型別有4個:emptyCtx/cancelCtx/timerCtx/valueCtx, 除了emptyCtx,實現Deadline()方法的只有timerCtx,(ps:這裡的實現特指有業務意義的實現), 唯一可以構造timerCtx的只有WithDeadline的第二步中. 這麼說來,頂層Context不支援deadline,最多第二層派生支援deadline的Context, 第三層派生用於將取消訊號進行傳播.

WithCancel上面已經分析了,派生一個支援取消訊號的Context,並將父Context的取消訊號 傳播到派生Context(ps:這麼說有點繞,簡單點講就是將派生Context新增到父Context的children), 下面看看第一個構造支援deadline的過程.

構造timerCtx,傳播取消訊號,判斷截至日期是否已過,如果沒過,利用time.AfterFunc建立定時器, 設定定時觸發的協程處理,之後返回派生Context和取消函數.

可以看到,整個WithDeadline是基於WithCancel實現的,截至日期到期後,利用取消訊號來做後續處理.

因為timerCtx是內嵌了cancelCtx,所以有一個派生Context是可以同時支援取消和deadline的, 後面的value支援也是如此.

WithDeadline的註釋說明: 派生Context的deadline不晚於引數,如果引數晚於父Context支援的deadline,使用父Context的deadline, 如果引數指定的比父Context早,或是父Context不支援deadline,那麼派生Context會構造一個新的timerCtx. 父Context的取消/派生Context的取消/或者deadline的過期,都會觸發取消訊號對應的操作執行, 具體就是Done()通道會被關閉.

func WithTimeout(parent Context,
  timeout time.Duration) (Context, CancelFunc) {
  return WithDeadline(parent, time.Now().Add(timeout))
}

WitchTimeout是基於WithDeadline實現的,是一種擴充套件,從設計上可以不加,但加了會增加呼叫者的便捷. WithTimeout可用在"慢操作"上.上下文使用完之後,應該立即呼叫取消操作來釋放資源.

支援值WitchValue:

func WithValue(parent Context, key, val interface{}) 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能比較,就構造一個valueCtx,用Value()獲取值時,如果和當前派生Context的key不匹配, 就會和父Context的key做匹配,如果不匹配,最後頂層Context會返回nil.

總結一下:如果是Value(),會一直通過派生Context找到頂層Context; 如果是deadline,會返回當前派生Context的deadline,但會受到父Context的deadline和取消影響; 如果是取消函數,會將傳播取消訊號的相關Context都做取消操作. 最重要的是Context是一個樹形結構,可以組成很複雜的結構.

到目前為止,只瞭解了包的內部實現(頂層Context的構造/With-系列函數的派生), 具體使用,需要看例子和實際測試.

ps:一個包內部如何複雜,對外暴露一定要簡潔.一個包是無法設計完美的,但是約束可以, 當大家都接受一個包,並接受使用包的規則時,這個包就成功了,context就是典型.

對於值,可以用WithValue派生,用Value取; 對於cancel/deadline,可以用WithDeadline/WithTimeout派生,通過Done訊號獲取結束訊號, 也可以手動用取消函數來觸發取消操作.整個包的功能就這麼簡單.

七、擴充套件功能以及如何擴充套件

擴充套件功能現在支援取消/deadline/value,擴充套件這個層級不應該放在這個包, 擴充套件Context,也就是新建Context的實現型別,這個是可以的, 同樣實現型別需要承載擴充套件功能,也不合適.

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

介面canceler是保證取消訊號可以在鏈上傳播,cancel方法由cancelCtx/timerCtx實現, Done只由cancelCtx建立done通道,不管是從功能上還是方法上都沒有擴充套件的必要.

剩下的就是Value擴充套件成多kv對,這個主要還是要看應用場景.

八、補充

Context被取消後Err返回Canceled錯誤,超時之後Err返回DeadlineExceeded錯誤, 這個DeadlineExceeded還有些說法: 

 var DeadlineExceeded error = deadlineExceededError{}

  type deadlineExceededError struct{}
  func (deadlineExceededError) Error() string {
    return "context deadline exceeded"
  }
  func (deadlineExceededError) Timeout() bool   { return true }
  func (deadlineExceededError) Temporary() bool { return true }

再看看net.Error介面:

type Error interface {
  error
  Timeout() bool   // Is the error a timeout?
  Temporary() bool // Is the error temporary?
}

context中的DeadlineExceeded預設是實現了net.Error介面的範例. 這個是為後面走網路超時留下的擴充套件.

到此這篇關於對Go語言中的context包原始碼分析的文章就介紹到這了,更多相關Go語言context包原始碼分析內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com