源碼面前,了無祕密。本文做爲context分析系列的第二篇,會從源碼的角度來分析context如何實現所承諾的功能及內在特性。本篇主要從如下四個角度闡述: context中的接口、context有哪些類型、context的傳遞實現、context的層級取消觸發實現。安全
上一篇go context剖析之使用技巧中能夠看到context包自己包含了數個導出函數,包括WithValue、WithTimeout等,不管是最初構造context仍是傳導context,最核心的接口類型都是context.Context,任何一種context也都實現了該接口,包括value context。bash
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
複製代碼
既然context都須要實現Context,那麼包括不直接可見(非導出)的結構體,一共有幾種context呢?答案是4種。併發
emptyCtx定義以下函數
type emptyCtx int
複製代碼
爲了減輕gc壓力,emptyCtx實際上是一個int,而且以do nothing的方式實現了Context接口,還記得context包裏面有兩個初始化context的函數post
func Background() Context
func TODO() Context
複製代碼
這兩個函數返回的實現類型即爲emptyCtx,而在contex包中實現了兩個emptyCtx類型的全局變量: background、todo,其定義以下ui
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
複製代碼
上述兩個函數依次對應這兩個全局變量。到這裏咱們能夠很肯定地說context的根節點就是一個int全局變量,而且Background()和TODO()是同樣的。因此千萬不要用nil做爲context,而且從易於理解的角度出發,未考慮清楚是否傳遞、如何傳遞context時用TODO,其餘狀況都用Background(),如請求入口初始化contextspa
cancelCtx的cancel機制是手工取消、超時取消的內部實現,其定義以下設計
type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
複製代碼
這裏的mu是context併發安全的關鍵、done是通知的關鍵、children存儲結構是內部最經常使用傳導context的方式。3d
timerCtx內部包含了cancelCtx,而後經過定時器,實現了到時取消的功能,定義以下code
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
複製代碼
這裏deadline只作記錄、String()等邊緣功能,timer纔是關鍵。
valueCtx是四個類型的最後一個,只用來傳值,固然也能夠傳遞,全部context均可以傳遞,定義以下
type valueCtx struct {
Context
key, val interface{}
}
複製代碼
因爲有的人認爲context應該只用來傳值、有的人認爲context的cancel機制纔是核心,因此對於valueCtx也在下面作了一個單獨的介紹,你們能夠經過把握內部實現後按照本身的業務場景作一個取捨(傳值能夠用一個全局結構體、map之類)。
在上面valueCtx的定義中,咱們能夠看出其實value context底層不是一個map,而是每個單獨的kv映射都對應一個valueCtx,當傳遞多個值時就要構造多個ctx。同時,這要是value contex不能自低向上傳遞值的緣由。
valueCtx的key、val都是接口類型,在調用WithValue的時候,內部會首先經過反射肯定key是否可比較類型(同map中的key),而後賦值key
在調用Value的時候,內部會首先在本context查找對應的key,若是沒有找到會在parent context中遞歸尋找,這也是value能夠自頂向下傳值的緣由。
首先能夠明確,任何一種context都具備傳遞性,而傳遞性的內在機制能夠理解爲: 在調用WithCancel、WithTimeout、WithValue時如何處理父子context。從傳遞性的角度來講,幾種With*函數內部都是經過propagateCancel這個函數來實現的,下面以WithCancel函數爲例
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
複製代碼
newCancelCtx是cancelCtx賦值父context的過程,而propagateCancel創建父子context之間的聯繫。
propagateCance定義以下
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {// context包內部能夠直接識別、處理的類型
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 {// context包內部不能直接處理的類型,好比type A struct{context.Context},這種靜默包含的方式
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
複製代碼
1.若是parent.Done是nil,則不作任何處理,由於parent context永遠不會取消,好比TODO()、Background()、WithValue等。 2.parentCancelCtx根據parent context的類型,返回bool型ok,ok爲真時須要創建parent對應的children,並保存parent->child映射關係(cancelCtx、timerCtx這兩種類型會創建,valueCtx類型會一直向上尋找,而循環往上找是由於cancel是必須的,而後找一種最合理的。),這裏children的key是canceler接口,並不能處理全部的外部類型,因此會有else,示例見上述代碼註釋處。對於其餘外部類型,不創建直接的傳遞關係。 parentCancelCtx定義以下
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context // 循環往上尋找
default:
return nil, false
}
}
}
複製代碼
上文在闡述傳遞性時的實現時,也包含了一部分取消機制的代碼,這裏不會再列出源碼,可是會依據上述源碼進行說明。對於幾種context,傳遞過程大同小異,可是取消機制有所不一樣,針對每種類型,我會一一解釋。不一樣類型的context能夠在一條鏈路進行取消,可是每個context的取消只會被一種條件觸發,因此下面會單獨介紹下每一種context的取消機制(組合取消的場景,按照先到先得的原則,不管那種條件觸發的,都會傳遞調用cancel)。這裏有兩個設計很關鍵:
cancelCtx會主動進行取消,在自頂向下取消的過程當中,會遍歷children context,而後依次主動取消。 cancel函數定義以下
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)
}
}
複製代碼
WithTimeout是經過WithDeadline來實現的,均對應timerCtx類型。經過parentCancelCtx函數的定義咱們知道,timerCtx也會記錄父子context關係。可是timerCtx是經過timer定時器觸發cancel調用的,部分實現以下
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
複製代碼
這裏暫時只想到了靜默包含即type A struct{context.Context}的狀況。經過parentCancelCtx和propagateCancel咱們知道這種context不會創建父子context的直接聯繫,可是會經過單獨的goroutine去檢測done channel,來肯定是否須要觸發鏈路上的cancel函數,實現見propagateCancel的else部分。
context的實現並不複雜,可是在實際開發中確能帶來不小的便利性。篇一力求你們可以按場景對號入座熟練地使用context,篇二但願你們可以從源碼層面瞭解到context的實現,在一些極端場景下,如靜默包含context,也能從容權衡利弊,作到知其然知其因此然,謝謝。