go語言程式設計:看完這篇再也不擔心不會用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介面。型別結構如下

go語言程式設計:看完這篇再也不擔心不會用context了

函式、結構體和變數說明

名稱型別可否匯出說明

Context介面可以Context最基本介面,定義了4個方法

canceler介面不可以Context取消介面,定義了2個方法

emptyCtx結構體不可以實現了Context介面,預設都是空實現,emptyCtx是int類型別名

cancelCtx結構體不可以可以被取消

valueCtx結構體不可以可以儲存key-value資訊

timerCtx結構體不可以可被取消,也可超時取消

CancelFunc函式可以取消函式簽名

Background函式可以返回一個空的Context,常用來作為根Context

Todo函式可以返回一個空的 context,常用於初期寫的時候,沒有合適的context可用

WithCancel函式可以理解為產生可取消Context的建構函式

WithDeadline函式可以理解為產生可超時取消Context的建構函式

WithTimeout函式可以理解為產生可超時取消Context的建構函式

WithValue函式可以理解為產生key-value Context的建構函式

newCancelCtx函式不可以建立一個可取消的Context

propagateCancel函式不可以向下傳遞 context 節點間的取消關係

parentCancelCtx函式不可以找到最先出現的一個可取消Context

removeChild函式不可以將當前的canceler從父Context中的children map中移除

background變數不可以包級Context,預設的Context,常作為頂級Context

todo變數不可以包級Context,預設的Context實現,也作為頂級Context,與background同類型

closedchan變數不可以channel struct{}型別,用於資訊通知

Canceled變數可以取消error

DeadlineExceeded變數可以超時error

cancelCtxKey變數不可以int類型別名,做標記用的

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介面與Context介面合併呢?況且他們定義的方法中都有Done方法,可以解釋得通的說法是,原始碼作者認為cancel方法並不是Context必須的,根據最小介面設計原則,將兩者分開。像

emptyCtx和

valueCtx不是可取消的,所以他們只要實現Context介面即可。

cancelCtx和

timerCtx是可取消的Context,他們要實現2個介面中的所有方法。

WithCancel提供了建立可取消Context方法,它有2個返回值,分別是Context型別和func()型別,Context(第一個返回值)在使用時一般會傳給其他協程,第二個返回值放在main協程或頂級協程中處理,實現了呼叫方caller和被調方callee隔離。callee只管負責收到caller傳送的取消資訊時執行退出操作。

// 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 intfunc (*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節點是不可被取消的節點,所以它就不存在取消說法。就是當父節點被取消的時候,它下面所有的子節點都會被取消。

go語言程式設計:看完這篇再也不擔心不會用context了

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

go語言程式設計:看完這篇再也不擔心不會用context了

// *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是一個*cancelCtxfunc (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中移除掉,因為父級節點並沒有取消。

go語言程式設計:看完這篇再也不擔心不會用context了

執行ctx5。cancel前

go語言程式設計:看完這篇再也不擔心不會用context了

執行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退出,防止洩露。

go語言程式設計:看完這篇再也不擔心不會用context了

// 查詢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。

go語言程式設計:看完這篇再也不擔心不會用context了

// 從parent位置沿著父級不斷的向上查詢,直到遇到第一個*cancelCtx或者不存這樣的*cancelCtxfunc 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”)會不斷查詢父節點,直到第二個父節點,查到結果返回。

go語言程式設計:看完這篇再也不擔心不會用context了

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 “”}

context最佳實踐

方法或函式的第一個引數傳遞context 首引數傳遞context物件,例如在net包中,是下面這樣定義Dialer。DialContext的。

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

通常不要將context放到結構體中 使用 context 的一個很好的心智模型是它應該在程式中流動,應該貫穿整個程式碼。不希望將其儲存在結構體之中。它從一個函式傳遞到另一個函式,並根據需要進行擴充套件。

使用WithValue的時候注意傳遞的value是執行緒安全的 withValue可能在多個goroutine中使用,而*withValue。value在賦值時無需加鎖保護,但是要確保對value操作的安全性,例如當value是一個map物件時,在每個groutine是不能修改的,那怎麼辦呢?當需要修改的時候,採用COW技術即寫時複製,將原map複製一份到新的,在新的上面修改。

對cancelCtx要記得呼叫cancel 不是中途放棄的時候,才去呼叫cancel,只要你的任務完成了,就需要呼叫cancel,這樣Context的資源才能釋放。

總結

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

歡迎關注微信公眾號—資料小冰,更多精彩內容和你一起分享