深入解析 Golang 之 context

context 是什麼

context 翻譯成中文就是上下文,在軟件開發環境中,是指接口之間或函數調用之間,除了傳遞業務參數之外的額外信息,像在微服務環境中,傳遞追蹤信息 traceID, 請求接收和返回時間,以及登錄操作用戶的身份等等。本文說的 context 是指 golang 標準庫中的 context 包。Go 標準庫中的 context 包,提供了 goroutine 之間的傳遞信息的機制,信號同步,除此之外還有超時 (timeout) 和取消 (cancel) 機制。概括起來,Context 可以控制子 goroutine 的運行,超時控制的方法調用,可以取消的方法調用。

爲什麼需要 context

根據前面的 Context 的介紹,Context 可以控制 goroutine 的運行,超時、取消方法的調用。對於這些功能,有沒有別的實現方法。當然是有的,控制 goroutine 的運行,可以通過 select+channel 的機制實現,超時控制也可以通過 ticker 實現,取消方法調用也可以向 channel 中發送信號,通知方法退出。既然 Context 能實現的功能,也有別的方式能夠實現,那爲啥還要 Context 呢?在一些複雜的場景中,通過 channel 等方式控制非常繁瑣,而採用 Context 可以很方便的實現上述功能。場景 1:主協程啓動了 m 個子協程,分別編號爲 g1,g2,...gm。對於 g1 協程,它又啓動了 n 個子協程,分別編號爲 g11,g12,...g1n。現在希望主協程取消的時候或 g1 取消的時候,g1 下面的所有子協程也取消執行,採用 channel 的方法,需要申請 2 個 channel, 一個是主協程退出通知的 channel, 另一個是 g1 退出時的 channel。g1 的所有子協程需要同時 select 這 2 個 channel。現在是 2 層,用 channel 還能接受,如果層級非常深,那監控起來需要很多的 channel, 操作非常繁瑣。採用 Context 可以簡單的達到上述效果,不用申請一堆 channel。場景 2: 在微服務中,任務 A 運行依賴於下游的任務 B, 考慮到任務 B 可能存在服務不可用,所以通常在任務 A 中會加入超時返回邏輯,需要開一個定時器,同時任務 A 也受控於父協程,當父協程退出時,希望任務 A 也退出,那麼在任務 A 中也要監控父協程通過 channle 發送的取消信息,那有沒有一種方式將這兩種情況都搞定,不用即申請定時器又申請 channel,因爲他們的目的都是取消任務 A 的運行嘛,Context 就能搞定這種場景。

context 源碼解析

下面的源碼解析的是 go 的最新版本 1.14.2

結構圖

context 定義了 2 大接口,Context和canceler, 結構體類型 * emptyCtx,*valueCtx 實現了 Context 接口,*cancelCtx 同時實現了 Context 接口和 cancelr 接口,*timerCtx 內嵌了 cancelCtx,它也間接實現了 Context 和 canceler 接口。類型結構如下

函數、結構體和變量說明

LtavBX

Context 接口

Context 具體實現包括 4 個方法,分別是 Deadline、Done、Err 和 Value, 如下所示,每個方法都加了註解說明。

// Context接口,下面定義的四個方法都是冪等的
type Context interface {
 // 返回這個Context被取消的截止時間,如果沒有設置截止時間,ok的值返回的是false,
 // 後續每次調用對象的Deadline方法是,返回的結果都和第一次相同,即具有冪等性
 Deadline() (deadline time.Time, ok bool)
 
 // 返回一個channel對象,在Context被取消時,此channel會被close。如果沒有被
 // 取消,可能返回nil。每次調用Done總是會返回相同的結果,當channel被close的時候,
 // 可以通過ctx.Err獲取錯誤信息
 Done() <-chan struct{}
 
 // 返回一個error對象,當channel沒有被close的時候,Err方法返回nil,如果channel被
 // close, Err方法會返回channel被close的原因,可能是被cancel,deadline或timeout取消
 Err() error
 
 // 返回此cxt中指定key對應的value
 Value(key interface{}) interface{}
}

canceler 接口

canceler 接口定義如下所示,如果一個 Context 類型實現了下面定義的 2 個方法,該 Context 就是一個可取消的 Context。Context 包中結構體指針*cancelCtx和*timerCtx實現了 canceler 接口。

// canceler接口,核心是cancel方法,Done()不能省略,propagateCancel中的child.Done()
//在使用,因爲Context接口中已有Done()方法了,它們的簽名是一模一樣的
// context包核心的兩個接口是這裏的canceler和前面的Context接口,爲啥不把這裏的canceler與
// Context合成一個接口呢?
// 1. 這裏可以看到作者的設計思想,cancel操作不是Context必須功能,像*valueCtx
//     只是傳遞數據信息,並不會有取消操作。
//  2. WithCancel提供給外部唯一創建*cancelCtx函數非常巧妙,它的返回值有2部分,分別是
//     Context類型和func()類型,這樣顯示的將Context的取消操作放到取消函數中(第二個返回值)
//     Context(第一個返回值)會傳給其他協程,第二個返回值放在main協程或頂級協程處理取消
//     caller只管負責取消,callee只關心取消時做什麼操作,caller通過發送消息通知callee。

//  canceler是不可導出的,外部不能直接操作canceler類型對象,只能通過func()操作。
//  *cancelCtx和*timerCtx實現了該接口
type canceler interface {
 cancel(removeFromParent bool, err error)
 // 這裏的Done()不能省略,propagateCancel中的child.Done()在使用
 Done() <-chan struct{}
}

Background/Todo

background 和 todo 是兩個全局 Context,實現方式都是返回 nil 值, 兩者都不可導出,通過包提供的 Background() 和 TODO() 導出供外部使用, 兩者都是不可取消的 Context, 通常都是放在 main 函數或者最頂層使用。

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

var (
 // background和todo是兩個全局Context,實現方式都是返回nil值
 // 兩者都不可導出,通過包提供的Background()和TODO()導出供外部使用
 // 兩者都是不可取消的Context,通常都是放在main函數或者最頂層使用
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
 return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
 return todo
}

cancelCtx

cancleCtx 結構字段比 emptyCtx 豐富多了,它內嵌了 Context 接口,在 golang 中,內嵌也就是繼承,當我們將一個實現了 Context 的結構體賦值給 cancleCtx 的時候,cancelCtx 也就實現了 Context 定義的 4 個方法。只不過 * cancelCtx 重寫了 Done、Err 和 Value 方法。mu 字段用於保護結構體中的字段,在訪問修改的時候進行加鎖處理,防止併發 data race 衝突。done 是一個 channel, 同關閉 close(done) 實現信息通知,當一個 channel 被關閉之後,它返回的是該類型的 nil 值,本處就是 struct{}。children 字段保存可取消的子節點,cancelCtx 可以級聯成一個樹形結構,如下圖所示:當 B 被取消的時候,掛在它下面的 G 也會被取消,E 節點是不可被取消的節點,所以它就不存在取消說法。就是當父節點被取消的時候,它下面所有的子節點都會被取消。

// cancelCtx是可取消的Context, 當它被取消的時候,它的孩子cancelCtx也都會被取消,也就是級聯取消
type cancelCtx struct {
 Context
 // 互斥鎖字段,保護下面字段,防止存在data race
 mu sync.Mutex // protects following fields
 // done表示是否取消標記,當done被取消,也就是close(done)之後,調用cancelCtx.Done()
 // 會直接返回
 done chan struct{} // created lazily, closed by first cancel call
 // 記錄可取消的孩子節點
 children map[canceler]struct{} // set to nil by the first cancel call
 // 當done沒有取消即沒有關閉的時候,err返回nil, 當done被關閉時,err返回非空值,err值的內容
 // 反映被關閉的原因,是主動cancel還是timeout取消
 err error // set to non-nil by the first cancel call
}

*cancelCtx.Value 方法返回的是 cancelCtx 的自身地址,只有當可被取消的類型是 context 中定義的 cancelCtx 時,纔會被返回,否則,遞歸查詢 c.Context.Value,直到最頂級的 emptyCtx,會返回 nil。結合下面的圖很好理解,ctx4.Value(&cancelCtxKey) 會返回它本身的地址 & ctx4。對於 ctx3.Value(&cancelCtxKey), 因爲它是 valueCtx, 結合 valueCtx.Value(key) 源碼可以看到,它的 key 不可能是 & cancelCtxKey, 因爲在包外是不能獲取到 cancelCtxKey 地址的,它是不可導出的,會走到 ctx3.Context.Value(&cancelCtxKey), 就是在執行 ctx2.Value(&cancelCtxKey), ctx2 是 cancelCtx,所以會返回 ctx2 的地址 & ctx2。

// *cancelCtx.Value方法看起來比較奇怪,將key與一個固定地址的cancelCtxKey比較
// cancelCtxKey是不可導出的,它是一個int變量,所以對外部包來說,調用*cancelCtx.Value
// 並沒有什麼實際意義。它是給內部使用的,在parentCancelCtx中有如下使用
// p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
// 可以看到傳入的key是cancelCtxKey的地址,那key==&cancelCtxKey肯定是成立的嘛
// 所以直接返回*cancelCtx。理順一下思路,就是*cancelCtx調用Value返回它本身,非*cancelCtx
// 調用Value是它自己的實現,肯定跟*cancelCtx是不一樣的,對非*cancelCtx調用c.Context.Value(&cancelCtxKey)
// 會一直遞歸查詢到最後的context(background/todo),返回的會是nil。
// 總結出來,*cancelCtx.Value並不是給外部使用的,它主要表示當前調用者的Context是一個*cancelCtx
func (c *cancelCtx) Value(key interface{}) interface{} {
 if key == &cancelCtxKey {
  return c
 }
 return c.Context.Value(key)
}

Done 方法用於通知該 Context 是否被取消,通過監聽 channel 關閉達到被取消通知目的,c.done 沒有被關閉的時候,調用 Done 方法會被阻塞,被關閉之後,調用 Done 方法返回 struct{}。這裏採用惰性初始化的方法,當 c.done 未初始化的時候,先初始化。

// 初始化的時候 *cancelCtx.done是未初始化的channel, 所以它的值是nil, 這裏判斷如果它是
// nil表明channel done還未初始化,先進行初始化。如果已初始化,返回的是c.done的值。這裏有2點
// 對於新手值得學習,1是c.done先賦值給一個臨時變量,return 的是臨時變量,不能直接return c.done
// 因爲這樣c.done會處於c.mu鎖之外,未起到保護作用。2是這裏採用惰性初始化方式,新創一個*cancelCtx的
// 時候沒有理解初始化,在使用*cancelCtx.Done中進行的初始化
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
}

cancel 方法通過關閉 * cancelCtx.done 達到通知 callee 的目的。如果 c.done 還未初始化,說明 Done 方法還未被調用,這時候直接將 c.done 賦值一個已關閉的 channel,Done 方法被調用的時候不會阻塞直接返回 strcut{}。然後遞歸對子節點進行 cancel 操作,最後將當前的 cancelCtx 從它所掛載的父節點中的 children map 中刪除。注意 removeFromParent 參數,對所有子節點進行 cancel 的時候,即下面的 child.cancle(false,err) 傳遞的是 false, 都會執行 c.children=nil 做清空操作,所以沒有必要傳 true, 在最外層 cancel funtion 被 cancel 的時候,removeFromParent 要傳 true,這裏需要將 cancelCtx 從它的父節點 children 中移除掉,因爲父級節點並沒有取消。

執行 ctx5.cancel 前

執行 ctx5.cancel 後

// 取消操作,通過關閉*cancelCtx.done達到通知的效果,WithCancel函數調用的時候
// 返回一個context和cancel function,cancel function是一個閉包函數,關聯了外層
// 的context,當 cancel function被調用的時候,實際執行的是 *cancelCtx.cancel函數
// 將*cancelCtx.done關閉,callee調用context.Done會返回,然後對掛在下面的children
// canceler執行遞歸操作,將所有的children自底向上取消。
// note: 這裏在遞歸取消子canceler的時候,removeFromParent傳遞參數爲false, 爲啥這樣寫呢?
//  因爲這裏所有子canceler的children都會執行c.children=nil,做清空操作,所有沒有必要傳true
//  進行removeChild(c.Context,c)操作了。
//  在最外層cancel function調用cancel的時候,removeFromParent要傳true, 這裏需要將*cancelCtx
//  從它的父級canceler中的children中移除掉,因爲父級canceler並沒有取消
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.
  // 子*cancelCtx不用執行removeChild()操作,自底向上遞歸清理了children.
  child.cancel(false, err)
 }
 c.children = nil
 c.mu.Unlock()

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

查找 child 的掛載點,找到第一個 * cancelCtx, 將 child 掛在它下面,如果父節點都是不可取消的,那就不存在掛載點,直接返回。還有一種情況,找到了可取消的 Context,但這個 Context 不是 cancelCtx, 這種可取消的 Context 是我們自定義結構體類型,它是沒有 children 的。對應下面的單獨開啓一個 goroutine 的代碼,監聽 parent.Done,當 parent 被取消的時候,取消下面的子節點,即 child.cancel。child.Done 是不能省略不寫的,當 child 取消的時候,這裏啓動的 groutine 退出,防止泄露。

// 查找child的掛載點,如果父級Context都是不可取消的,直接返回,因爲不存在這樣的掛載點
// 從parent中沿着父級向上查找第一個*cancelCtx,找到了就將child添加到
// p.children中,如果沒有找到*cancelCtx, 但是一個別類型的可取消Context,啓動一個
// goroutine單獨處理

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)
  // 走到這裏表示找到了一個可取消的Context(done非nil), 但這個可取消的Context
  // 並不是*cancelCtx, 那這個Context是啥呢?它可能是我們自己實現的可取消的Context類型
  // 他是沒有children map 字段的,當它被取消的時候,要通知子Context取消,即要執行child.cancel
  // 這裏的 case <- parent.Done()不能省略
  go func() {
   select {
   // 這裏的parent.Done()也是不能省略的,當parent Context取消的時候,要取消下面的子Context child
   // 如果去掉,就不能級聯取消子Context了。
   case <-parent.Done():
    // 因爲父級Context並不是*cancelCtx,也就不存在p.children, 不用執行removeChild操作,
    // 這裏直接傳false
    child.cancel(false, parent.Err())

   // 當child取消的時候,這裏啓動的groutine退出,防止泄露
   case <-child.Done():
   }
  }()
 }
}

parentCancel 查找 parent 的第一個 * cancelCtx,如果 done 爲 nil 表示是不可取消的 Context,如果 done 爲 closedchan 表示 Context 已經被取消了,這兩種情況可以直接返回,不存 cancelCtx 了。parent.Value(&cancelCtxKey) 遞歸向上查找節點是不是 cancelCtx。注意這裏 p.done==done 的判斷,是防止下面的情況,parent.Done 找到的可取消 Context 是我們自定義的可取消 Context, 這樣 parent.Done 返回的 done 和 cancelCtx 肯定不在一個同級,它們的 done 肯定是不同的。這種情況也返回 nil。

// 從parent位置沿着父級不斷的向上查找,直到遇到第一個*cancelCtx或者不存這樣的*cancelCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
 done := parent.Done()
 // done=closedchan 表示父級可取消的Context已取消,可以自己返回了
 // done=nil 表示一直向上查找到了頂級的background/todo Context, 也可以直接返回了
 if done == closedchan || done == nil {
  return nil, false
 }
 // 遞歸向上查詢第一個*cancelCtx
 p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
 if !ok {
  return nil, false
 }
 p.mu.Lock()
 // 這裏爲啥要判斷 p.done==done, 見源碼分析說明
 ok = p.done == done
 p.mu.Unlock()
 if !ok {
  return nil, false
 }
 return p, true
}

removeChild 比較簡單,將 child 從 parent 最先遇到的 * cancelCtx 中的 children map 中刪除。

// 從parent中找到最先遇到的*cancelCtx, 這個是child的掛載點,將child從最先遇到的*cancelCtx 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()
}

timerCtx

timerCtx 內嵌有 cancelCtx, 所以它是一個可取消的 Context, 此外它有超時定時器和超時截止時間字段,對 timer 和 deadlien 的訪問,是通過 cancelCtx.mu 加鎖防止 data race 的。

// timeCtx超時取消Context,內嵌有cancelCtx,所以間接實現了Context接口
type timerCtx struct {
 cancelCtx

 // 超時定時器
 timer *time.Timer // Under cancelCtx.mu.

 // 超時截止時間
 deadline time.Time
}

WithDeadline 是創建 timerCtx 的構造函數,用於返回一個可超時取消的 Context。

// 可以理解爲創建超時Context的構造函數,需要傳入一個超時接着時間,創建了一個*timeCtx類型
// 通過*timeCtx結構體定義可以看到,它內嵌了一個cancelCtx類型,這裏需要注意下,雖然內嵌的
// 是cancelCtx類型,但是他是實現了Context接口的,因爲cancelCtx中內嵌有Context,所以
// cancelCtx實現了Context接口,只不過重寫了*cancelCtx.Done(), *cancel.Err(), *cancel.Value()實現
// 進一步timerCtx內嵌有cancelCtx,所以timerCtx也實現了Context接口
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 // 父級Context的超時時間比d早,直接創建一個可取消的context, 原因是父級context比子
 // context先超時,當父級超時時,會自動調用cancel函數,子級context也會被取消了。所以
 // 不用單獨處理子級context的定時器到時之後,自動調用cancel函數。
 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,
 }
 // 同cancelCtx的操作相同 ,將當前的c掛到父級ontext節點上
 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 {
  // 啓動一個定時器,在dur時間後,自動進行取消操作
  c.timer = time.AfterFunc(dur, func() {
   c.cancel(true, DeadlineExceeded)
  })
 }
 return c, func() { c.cancel(true, Canceled) }
}

Deadline 方法返回 timerCtx 是否設置了超時截止日期,這裏始終返回 true, 因爲通過 WithTimeout 和 WithDeadline 創建的 * timerCtx 都設置了超時時間。

// *timeCtx重寫了Deadline實現,方法會返回這個
// Context 被取消的截止日期。如果沒有設置截止日期,
// ok 的值 是 false。後續每次調用這個對象的 Deadline 方法時,
// 都會返回和第一次調用相同的結果
// note:這裏ok爲啥直接返回true呢?因爲通過創建*timeCtx的兩個方法WithDeadline
//      和WithTimeout都設置了*timeCtx.deadline值
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
 return c.deadline, true
}

*timerCtx 重寫了 cancel 的 cancel 方法,除了執行 timerCtx.cancelCtx.cancel, 將子 context 取消,然後做定時器的停止並清空操作。

// 取消操作, *timerCtx重寫了cancel的cancel, 先會執行*timeCtx.cancelCtx.cancel, 將
// 子級context取消,然後將當前的*timerCtx從父級Context移除掉
// 最後將定時器停止掉並清空
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()
}

WithTimeout 是對 WithDeadline 的包裝,將 timeout 轉換成了 deadline。

// 提供了創建超時Context的構造函數,內部調用的是WithDeadline, 創建的都是*timerCtx類型。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
 return WithDeadline(parent, time.Now().Add(timeout))
}

valueCtx

key-value Context, 用於傳輸信息的 Context,key 和 value 的賦值與訪問並沒有加鎖處理,因爲不需要,具體原因見 * valueCtx.Value 處的說明。

// 在協程中傳遞信息Context, key和value分別對應傳遞信息的鍵值對
// Note: 可以看到valueCtx中並沒有鎖結構對key,value賦值(WithValue函數)和讀取(Value函數)操作時進行保護
// 爲什麼不用加鎖,原因見*valueCtx.Value處的解析說明。
type valueCtx struct {
 Context
 key, val interface{}
}

WithValue 返回 key-value Context, 這裏傳入的 key 要是可進行比較的。

// WithValue函數是產生*valueCtx的唯一方法,即該函數是*valueCtx的構造函數。
// key不能爲空且是可以比較的,在golang中int、float、string、bool、complex、pointer、
// channel、interface、array是可以比較的,slice、map、function是不可比較的,
// 複合類型中帶有不可比較的類型,該複合類型也是不可比較的。
func WithValue(parent Context, key, val interface{}) Context {
 if key == nil {
  panic("nil key")
 }
 if !reflectlite.TypeOf(key).Comparable() {
  panic("key is not comparable")
 }
 return &valueCtx{parent, key, val}
}

*valueCtx.Value 遞歸查詢 key, 從當前節點查詢給定的 key,如果 key 不存在,繼續查詢父節點,如果都不存在,一直查詢到根節點,根節點通常都是 Background/TODO,返回 nil。

// Value函數提供根據鍵查詢值的功能,valueCtx組成了一個鏈式結構,可以理解成一個頭插法創建的單鏈表,
// Value函數從當前的Context查詢key,如果沒有查到,繼續查詢valueCxt的Context是否有對應的key ,
// 可以想象成從當前鏈表節點,向後順序查詢後繼節點是否存在對應的key, 直到尾節點(background或todo Context)
// background/todo Value返回的nil
// Value操作沒有加鎖處理,因爲傳遞給子協程的valueCtx進行Value操作時,其它協程不會對valueCtx進行修改操作,這個
// valueCtx是這個只讀的Context,所以在valueCtx中對key和value的操作沒有進行加鎖保護處理,因爲不存在data race.

func (c *valueCtx) Value(key interface{}) interface{} {
 // 要查詢的key與當前的valueCtx(c)中的key相同,直接返回
 if c.key == key {
  return c.val
 }
 // 否則遞歸查詢c中的Context,如果所有的Context都沒有,則最後會走到background/todo Context,
 // background/todo Context的Value函數直接返回的是nil
 return c.Context.Value(key)
}

valueCtx 實現了鏈式查找。如果不存在,還會向 parent Context 去查 找,如果 parent 還是 valueCtx 的話,還是遵循相同的原則: valueCtx 會嵌入 parent, 所以還是會查找 parent 的 Value 方法的, 下面的 ctx.Value("key1") 會不斷查詢父節點,直到第二個父節點,查到結果返回。

func main() {
 ctx := context.Background()
 ctx = WithValue(ctx, "key1", "01")
 ctx = WithValue(ctx, "key2", "02")
 ctx = WithValue(ctx, "key3", "03")
 ctx = WithValue(ctx, "key4", "04")

 fmt.Println(ctx.Value("key1"))
}
// valueCtx還實現了String() string簽名函數,該簽名是fmt包中一個接口,也就說
// valueCtx實現了fmt中的print接口,可以直接傳參給fmt.Println(valueCtx)進行打印
// 當前也可以直接fmt.Println(valueCtx.String())打印。
func (c *valueCtx) String() string {
 return contextName(c.Context) + ".WithValue(type " +
  reflectlite.TypeOf(c.key).String() +
  ", val " + stringify(c.val) + ")"
}
// stringify只給*valueCtx.String()使用,在*valueCtx.String()函數中,調用了
// stringify(v.val), v.val要麼是string類型,要麼實現了stringer接口,
// stringer接口定義了一個方法 String() string
// 即v.val要麼是string類型, 要麼該類型實現了 String() string 方法
func stringify(v interface{}) string {
 switch s := v.(type) {
 case stringer:
  return s.String()
 case string:
  return s
 }
 return "<not Stringer>"
}

context 最佳實踐

func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
  ...
}

總結

用 Context 來取消一個 goroutine 的運行,這是 Context 最常用的場景之一,Context 也被稱爲 goroutine 生命週期範圍 (goroutine-scoped) 的 Context,把 Context 傳遞給 goroutine。但是,callee goroutine 需要嘗試檢查 Context 的 Done 是否關閉了 對帶超時功能 context 的調用,比如通過 grpc 訪問遠程的一個微服務,超時並不意味着你會通知遠程微服務已經取消了這次調用,大概率的實現只是避免客戶端的長時間等待,遠程的服務器依然還執行着你的請求。

深入 Go 併發模型: Context[1] 深度解密 go 語言之 context[2]

Reference

[1] 深入 Go 併發模型: Context: https://zhuanlan.zhihu.com/p/75556488

[2] 深度解密 go 語言之 context: https://www.cnblogs.com/qcrao-2018/p/11007503.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WiZ9gLj6hJy233SIDNjY6w