Go語(yǔ)言學(xué)習(xí)之context包的用法詳解
前言
日常 Go 開發(fā)中,Context 包是用的最多的一個(gè)了,幾乎所有函數(shù)的第一個(gè)參數(shù)都是 ctx,那么我們?yōu)槭裁匆獋鬟f Context 呢,Context 又有哪些用法,底層實(shí)現(xiàn)是如何呢?相信你也一定會(huì)有探索的欲望,那么就跟著本篇文章,一起來(lái)學(xué)習(xí)吧!
需求一
開發(fā)中肯定會(huì)調(diào)用別的函數(shù),比如 A 調(diào)用 B,在調(diào)用過(guò)程中經(jīng)常會(huì)設(shè)置超時(shí)時(shí)間,比如超過(guò)2s 就不等待 B 的結(jié)果了,直接返回,那么我們需要怎么做呢?
//?睡眠5s,模擬長(zhǎng)時(shí)間操作 func?FuncB()?(interface{},?error)?{ ?time.Sleep(5?*?time.Second) ?return?struct{}{},?nil } func?FuncA()?(interface{},?error)?{ ?var?res?interface{} ?var?err?error ?ch?:=?make(chan?interface{}) ??//?調(diào)用FuncB(),將結(jié)果保存至?channel?中 ?go?func()?{ ??res,?err?=?FuncB() ??ch?<-?res ?}() ??//?設(shè)置一個(gè)2s的定時(shí)器 ?timer?:=?time.NewTimer(2?*?time.Second) ?? ??//?監(jiān)測(cè)是定時(shí)器先結(jié)束,還是?FuncB?先返回結(jié)果 ?select?{ ???? ????//?超時(shí),返回默認(rèn)值 ?case?<-timer.C: ??return?"default",?err ???? ????//?FuncB?先返回結(jié)果,關(guān)閉定時(shí)器,返回?FuncB?的結(jié)果 ?case?r?:=?<-ch: ??if?!timer.Stop()?{ ???<-timer.C ??} ??return?r,?err ?} } func?main()?{ ?res,?err?:=?FuncA() ?fmt.Println(res,?err) }
上面我們的實(shí)現(xiàn),可以實(shí)現(xiàn)超過(guò)等待時(shí)間后,A 不等待 B,但是 B 并沒(méi)有感受到取消信號(hào),如果 B 是個(gè)計(jì)算密度型的函數(shù),我們也希望B 感知到取消信號(hào),及時(shí)取消計(jì)算并返回,減少資源浪費(fèi)。
另一種情況,如果存在多層調(diào)用,比如A 調(diào)用 B、C,B 調(diào)用 D、E,C調(diào)用 E、F,在超過(guò) A 的超時(shí)時(shí)間后,我們希望取消信號(hào)能夠一層層的傳遞下去,后續(xù)所有被調(diào)用到的函數(shù)都能感知到,及時(shí)返回。
需求二
在多層調(diào)用的時(shí)候,A->B->C->D,有些數(shù)據(jù)需要固定傳輸,比如 LogID,通過(guò)打印相同的 LogID,我們就能夠追溯某一次調(diào)用,方便問(wèn)題的排查。如果每次都需要傳參的話,未免太麻煩了,我們可以使用 Context 來(lái)保存。通過(guò)設(shè)置一個(gè)固定的 Key,打印日志時(shí)從中取出 value 作為 LogID。
const?LogKey?=?"LogKey" //?模擬一個(gè)日志打印,每次從?Context?中取出?LogKey?對(duì)應(yīng)的?Value?作為L(zhǎng)ogID type?Logger?struct{} func?(logger?*Logger)?info(ctx?context.Context,?msg?string)?{ ?logId,?ok?:=?ctx.Value(LogKey).(string) ?if?!ok?{ ??logId?=?uuid.New().String() ?} ?fmt.Println(logId?+?"?"?+?msg) } var?logger?Logger //?日志打印?并?調(diào)用?FuncB func?FuncA(ctx?context.Context)?{ ?logger.info(ctx,?"FuncA") ?FuncB(ctx) } func?FuncB(ctx?context.Context)?{ ?logger.info(ctx,?"FuncB") } //?獲取初始化的,帶有?LogID?的?Context,一般在程序入口做 func?getLogCtx(ctx?context.Context)?context.Context?{ ?logId,?ok?:=?ctx.Value(LogKey).(string) ?if?ok?{ ??return?ctx ?} ?logId?=?uuid.NewString() ?return?context.WithValue(ctx,?LogKey,?logId) } func?main()?{ ?ctx?=?getLogCtx(context.Background()) ?FuncA(ctx) }
這利用到了本篇文章講到的 valueCtx,繼續(xù)往下看,一起來(lái)學(xué)習(xí) valueCtx 是怎么實(shí)現(xiàn)的吧!
Context 接口
type?Context?interface?{ ?Deadline()?(deadline?time.Time,?ok?bool) ?Done()?<-chan?struct{} ?Err()?error ?Value(key?interface{})?interface{} }
Context 接口比較簡(jiǎn)單,定義了四個(gè)方法:
- Deadline() 方法返回兩個(gè)值,deadline 表示 Context 將會(huì)在什么時(shí)間點(diǎn)取消,ok 表示是否設(shè)置了deadline。當(dāng) ok=false 時(shí),表示沒(méi)有設(shè)置deadline,那么此時(shí) deadline 將會(huì)是個(gè)零值。多次調(diào)用這個(gè)方法返回同樣的結(jié)果。
- Done() 返回一個(gè)只讀的 channel,類型為 chan struct{},如果當(dāng)前的 Context 不支持取消,Done 返回 nil。我們知道,如果一個(gè) channel 中沒(méi)有數(shù)據(jù),讀取數(shù)據(jù)會(huì)阻塞;而如果channel被關(guān)閉,則可以讀取到數(shù)據(jù),因此可以監(jiān)聽 Done 返回的 channel,來(lái)獲取 Context 取消的信號(hào)。
- Err() 返回 Done 返回的 channel 被關(guān)閉的原因。當(dāng) channel 未被關(guān)閉時(shí),Err() 返回 nil;channel 被關(guān)閉時(shí)則返回相應(yīng)的值,比如 Canceled 、DeadlineExceeded。Err() 返回一個(gè)非 nil 值之后,后面再次調(diào)用會(huì)返回相同的值。
- Value() 返回 Context 保存的鍵值對(duì)中,key 對(duì)應(yīng)的 value,如果 key 不存在則返回 nil。
Done() 是一個(gè)比較常用的方法,下面是一個(gè)比較經(jīng)典的流式處理任務(wù)的示例:監(jiān)聽 ctx.Done() 是否被關(guān)閉來(lái)判斷任務(wù)是否需要取消,需要取消則返回相應(yīng)的原因;沒(méi)有取消則將計(jì)算的結(jié)果寫入到 out channel中。
?func?Stream(ctx?context.Context,?out?chan<-?Value)?error?{ ??for?{ ???? ????//?處理數(shù)據(jù) ???v,?err?:=?DoSomething(ctx) ???if?err?!=?nil?{ ????return?err ???} ???? ????//?ctx.Done()?讀取到數(shù)據(jù),說(shuō)明獲取到了任務(wù)取消的信號(hào) ???select?{ ???case?<-ctx.Done(): ????return?ctx.Err() ????//?否則將結(jié)果輸出,繼續(xù)計(jì)算 ???case?out?<-?v: ???} ??} ?}
Value() 也是一個(gè)比較常用的方法,用于在上下文中傳遞一些數(shù)據(jù)。使用 context.WithValue() 方法存入 key 和 value,通過(guò) Value() 方法則可以根據(jù) key 拿到 value。
func?main()?{ ?ctx?:=?context.Background() ?c?:=?context.WithValue(ctx,?"key",?"value") ?v,?ok?:=?c.Value("key").(string) ?fmt.Println(v,?ok) }
emptyCtx
Context 接口并不需要我們自己去手動(dòng)實(shí)現(xiàn),一般我們都是直接使用 context 包中提供的 Background() 方法和 TODO() 方法,來(lái)獲取最基礎(chǔ)的 Context。
var?( ?background?=?new(emptyCtx) ?todo???????=?new(emptyCtx) ) func?Background()?Context?{ ?return?background } func?TODO()?Context?{ ?return?todo }
Background() 方法一般用在 main 函數(shù),或者程序的初始化方法中;在我們不知道使用哪個(gè) Context,或者上文沒(méi)有傳遞 Context時(shí),可以使用 TODO()。
Background() 和 TODO() 都是基于 emptyCtx 生成的,從名字可以看出來(lái),emptyCtx 是一個(gè)空的Context,沒(méi)有 deadline、不能被取消、沒(méi)有鍵值對(duì)。
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" }
除了上面兩個(gè)最基本的 Context 外,context 包中提供了功能更加豐富的 Context,包括 valueCtx、cancelCtx、timerCtx,下面我們就挨個(gè)來(lái)看下。
valueCtx
使用示例
我們一般使用 context.WithValue() 方法向 Context 存入鍵值對(duì),然后通過(guò) Value() 方法根據(jù) key 得到 value,此種功能的實(shí)現(xiàn)就依賴 valueCtx。
func?main()?{ ?ctx?:=?context.Background() ?c?:=?context.WithValue(ctx,?"myKey",?"myValue") ?v1?:=?c.Value("myKey") ?fmt.Println(v1.(string)) ?v2?:=?c.Value("hello") ?fmt.Println(v2)?//??nil }
類型定義
valueCtx 結(jié)構(gòu)體中嵌套了 Context,使用 key 、value 來(lái)保存鍵值對(duì):
type?valueCtx?struct?{ ?Context ?key,?val?interface{} }
WithValue
context包 對(duì)外暴露了 WithValue 方法,基于一個(gè) parent context 來(lái)創(chuàng)建一個(gè) valueCtx。從下面的源碼中可以看出,key 必須是可比較的!
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} }
*valueCtx 實(shí)現(xiàn)了 Value(),可以根據(jù) key 得到 value。這是一個(gè)向上遞歸尋找的過(guò)程,如果 key 不在當(dāng)前 valueCtx 中,會(huì)繼續(xù)向上找 parent Context,直到找到最頂層的 Context,一般最頂層的是 emptyCtx,而 emtpyCtx.Value() 返回 nil。
func?(c?*valueCtx)?Value(key?interface{})?interface{}?{ ?if?c.key?==?key?{ ??return?c.val ?} ?return?c.Context.Value(key) }
cancelCtx
cancelCtx 是一個(gè)用于取消任務(wù)的 Context,任務(wù)通過(guò)監(jiān)聽 Context 是否被取消,來(lái)決定是否繼續(xù)處理任務(wù)還是直接返回。
如下示例中,我們?cè)?main 函數(shù)定義了一個(gè) cancelCtx,并在 2s 后調(diào)用 cancel() 取消 Context,即我們希望 doSomething() 在 2s 內(nèi)完成任務(wù),否則就可以直接返回,不需要再繼續(xù)計(jì)算浪費(fèi)資源了。
doSomething() 方法內(nèi)部,我們使用 select 監(jiān)聽任務(wù)是否完成,以及 Context 是否已經(jīng)取消,哪個(gè)先到就執(zhí)行哪個(gè)分支。方法模擬了一個(gè) 5s 的任務(wù),main 函數(shù)等待時(shí)間是2s,因此沒(méi)有完成任務(wù);如果main函數(shù)等待時(shí)間改為10s,則任務(wù)完成并會(huì)返回結(jié)果。
這只是一層調(diào)用,真實(shí)情況下可能會(huì)有多級(jí)調(diào)用,比如 doSomething 可能又會(huì)調(diào)用其他任務(wù),一旦 parent Context 取消,后續(xù)的所有任務(wù)都應(yīng)該取消。
func?doSomething(ctx?context.Context)?(interface{},?error)?{ ?res?:=?make(chan?interface{}) ?go?func()?{ ??fmt.Println("do?something") ??time.Sleep(time.Second?*?5) ??res?<-?"done" ?}() ?select?{ ?case?<-ctx.Done(): ??return?nil,?ctx.Err() ?case?value?:=?<-res: ??return?value,?nil ?} } func?main()?{ ?ctx,?cancel?:=?context.WithCancel(context.Background()) ?go?func()?{ ??time.Sleep(time.Second?*?2) ??cancel() ?}() ?res,?err?:=?doSomething(ctx) ?fmt.Println(res,?err)?//?nil?,?context?canceled }
接下來(lái)就讓我們來(lái)研究下,cancelCtx 是如何實(shí)現(xiàn)取消的吧
類型定義
- canceler 接口包含 cancel() 和 Done() 方法,*cancelCtx 和 *timerCtx 均實(shí)現(xiàn)了這個(gè)接口。
- closedchan 是一個(gè)被關(guān)閉的channel,可以用于后面 Done() 返回
- canceled 是一個(gè) err,用于 Context 被取消的原因
type?canceler?interface?{ ?cancel(removeFromParent?bool,?err?error) ?Done()?<-chan?struct{} } //?closedchan?is?a?reusable?closed?channel. var?closedchan?=?make(chan?struct{}) func?init()?{ ?close(closedchan) } var?Canceled?=?errors.New("context?canceled")
CancelFunc 是一個(gè)函數(shù)類型定義,是一個(gè)取消函數(shù),有如下規(guī)范:
- CancelFunc 告訴一個(gè)任務(wù)停止工作
- CancelFunc 不會(huì)等待任務(wù)結(jié)束
- CancelFunc 支持并發(fā)調(diào)用
- 第一次調(diào)用后,后續(xù)的調(diào)用不會(huì)產(chǎn)生任何效果
type?CancelFunc?func()
&cancelCtxKey 是一個(gè)固定的key,用來(lái)返回 cancelCtx 自身
var?cancelCtxKey?int
cancelCtx
cancelCtx 是可以被取消的,它嵌套了 Context 接口,實(shí)現(xiàn)了 canceler 接口。cancelCtx 使用 children 字段保存同樣實(shí)現(xiàn) canceler 接口的子節(jié)點(diǎn),當(dāng) cancelCtx 被取消時(shí),所有的子節(jié)點(diǎn)也會(huì)取消。
type?cancelCtx?struct?{ ?Context ?mu???????sync.Mutex????????????//?保護(hù)如下字段,保證線程安全 ?done?????atomic.Value??????????//?保存?channel,懶加載,調(diào)用?cancel?方法時(shí)會(huì)關(guān)閉這個(gè)?channel ?children?map[canceler]struct{}?//?保存子節(jié)點(diǎn),第一次調(diào)用?cancel?方法時(shí)會(huì)置為?nil ?err??????error?????????????????//?保存為什么被取消,默認(rèn)為nil,第一次調(diào)用?cancel?會(huì)賦值 }
*cancelCtx 的 Value() 方法 和 *valueCtx 的 Value() 方法類似,只不過(guò)加了個(gè)固定的key: &cancelCtxKey。當(dāng)key 為 &cancelCtxKey 時(shí)返回自身
func?(c?*cancelCtx)?Value(key?interface{})?interface{}?{ ?if?key?==?&cancelCtxKey?{ ??return?c ?} ?return?c.Context.Value(key) }
*cancelCtx 的 done 字段是懶加載的,只有在調(diào)用 Done() 方法 或者 cancel() 時(shí)才會(huì)賦值。
func?(c?*cancelCtx)?Done()?<-chan?struct{}?{ ?d?:=?c.done.Load() ?? ??//?如果已經(jīng)有值了,直接返回 ?if?d?!=?nil?{ ??return?d.(chan?struct{}) ?} ?? ??//?沒(méi)有值,加鎖賦值 ?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{}) }
Err 方法返回 cancelCtx 的 err 字段
func?(c?*cancelCtx)?Err()?error?{ ???c.mu.Lock() ???err?:=?c.err ???c.mu.Unlock() ???return?err }
WithCancel
那么我們?nèi)绾涡陆ㄒ粋€(gè) cancelCtx呢?context 包提供了 WithCancel() 方法,讓我們基于一個(gè) Context 來(lái)創(chuàng)建一個(gè) cancelCtx。WithCancel() 方法返回兩個(gè)字段,一個(gè)是基于傳入的 Context 生成的 cancelCtx,另一個(gè)是 CancelFunc。
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)?} }
WithCancel 調(diào)用了兩個(gè)外部方法:newCancelCtx 、propagateCancel。newCancelCtx 比較簡(jiǎn)單,根據(jù)傳入的 context,返回了一個(gè) cancelCtx 結(jié)構(gòu)體。
func?newCancelCtx(parent?Context)?cancelCtx?{ ?return?cancelCtx{Context:?parent} }
propagateCancel 從名字可以看出,就是將 cancel 傳播。如果父Context支持取消,那么我們需要建立一個(gè)通知機(jī)制,這樣父節(jié)點(diǎn)取消的時(shí)候,通知子節(jié)點(diǎn)也取消,層層傳播。
在 propagateCancel 中,如果 父Context 是 cancelCtx 類型且未取消,會(huì)將 子Context 掛在它下面,形成一個(gè)樹結(jié)構(gòu);其余情況都不會(huì)掛載。
func?propagateCancel(parent?Context,?child?canceler)?{ ?? ??//?如果?parent?不支持取消,那么就不支持取消傳播,直接返回 ?done?:=?parent.Done() ?if?done?==?nil?{ ??return? ?} ??//?到這里說(shuō)明?done?不為?nil,parent?支持取消 ?? ?select?{ ?case?<-done: ??//?如果?parent?此時(shí)已經(jīng)取消了,那么直接告訴子節(jié)點(diǎn)也取消 ??child.cancel(false,?parent.Err()) ??return ?default: ?} ??//?到這里說(shuō)明此時(shí)?parent?還未取消 ?? ??//?如果?parent?是未取消的?cancelCtx? ?if?p,?ok?:=?parentCancelCtx(parent);?ok?{ ???? ????//?加鎖,防止并發(fā)更新 ??p.mu.Lock() ???? ????//?再次判斷,因?yàn)橛锌赡苌弦粋€(gè)獲得鎖的進(jìn)行了取消操作。 ????//?如果?parent?已經(jīng)取消了,那么子節(jié)點(diǎn)也直接取消 ??if?p.err?!=?nil?{ ???child.cancel(false,?p.err) ??}?else?{ ??????//?把子Context?掛到父節(jié)點(diǎn)?parent?cancelCtx?的?children字段下 ??????//?之后?parent?cancelCtx?取消時(shí),能通知到所有的?子Context? ???if?p.children?==?nil?{ ????p.children?=?make(map[canceler]struct{}) ???} ???p.children[child]?=?struct{}{} ??} ??p.mu.Unlock() ?}?else?{ ???? ???//?parent?不是?cancelCtx?類型,可能是用戶自己實(shí)現(xiàn)的Context ??atomic.AddInt32(&goroutines,?+1) ????//?啟動(dòng)一個(gè)協(xié)程監(jiān)聽,如果?parent?取消了,子?Context?也取消 ??go?func()?{ ???select?{ ???case?<-parent.Done(): ????child.cancel(false,?parent.Err()) ???case?<-child.Done(): ???} ??}() ?} }
cancel 方法就是來(lái)取消 cancelCtx,主要的工作是:關(guān)閉c.done 中的channel,給 err 賦值,然后級(jí)聯(lián)取消所有 子Context。如果 removeFromParent 為 true,會(huì)從父節(jié)點(diǎn)中刪除以該節(jié)點(diǎn)為樹頂?shù)臉洹?/p>
cancel() 方法只負(fù)責(zé)自己管轄的范圍,即自己以及自己的子節(jié)點(diǎn),然后根據(jù)配置判斷是否需要從父節(jié)點(diǎn)中移除自己為頂點(diǎn)的樹。如果子節(jié)點(diǎn)還有子節(jié)點(diǎn),那么由子節(jié)點(diǎn)負(fù)責(zé)處理,不用自己負(fù)責(zé)了。
propagateCancel() 中有三處調(diào)用了 cancel() 方法,傳入的 removeFromParent 都為 false,是因?yàn)楫?dāng)時(shí)根本沒(méi)有掛載,不需要移除。而 WithCancel 返回的 CancelFunc ,傳入的 removeFromParent 為 true,是因?yàn)檎{(diào)用 propagateCancel 有可能產(chǎn)生掛載,當(dāng)產(chǎn)生掛載時(shí),調(diào)用 cancel() 就需要移除了。
func?(c?*cancelCtx)?cancel(removeFromParent?bool,?err?error)?{ ?? ??//?err?是指取消的原因,必傳,cancelCtx?中是?errors.New("context?canceled") ?if?err?==?nil?{ ??panic("context:?internal?error:?missing?cancel?error") ?} ?? ??//?涉及到保護(hù)字段值的修改,都需要加鎖 ?c.mu.Lock() ?? ??//?如果該Context已經(jīng)取消過(guò)了,直接返回。多次調(diào)用cancel,不會(huì)產(chǎn)生額外效果 ?if?c.err?!=?nil?{ ??c.mu.Unlock() ??return? ?} ?? ??//?給?err?賦值,這里?err?一定不為?nil ?c.err?=?err ?? ??//?close?channel ?d,?_?:=?c.done.Load().(chan?struct{}) ??//?因?yàn)閏.done?是懶加載,有可能存在?nil?的情況 ??//?如果?c.done?中沒(méi)有值,直接賦值?closedchan;否則直接?close ?if?d?==?nil?{ ??c.done.Store(closedchan) ?}?else?{ ??close(d) ?} ?? ??//?遍歷當(dāng)前?cancelCtx?所有的子Context,讓子節(jié)點(diǎn)也?cancel ??//?因?yàn)楫?dāng)前的Context?會(huì)主動(dòng)把子Context移除,子Context?不用主動(dòng)從parent中脫離 ??//?因此?child.cancel?傳入的?removeFromParent?為false ?for?child?:=?range?c.children?{ ??child.cancel(false,?err) ?} ??//?將?children?置空,相當(dāng)于移除自己的所有子Context ?c.children?=?nil ?c.mu.Unlock() ? ??//?如果當(dāng)前?cancelCtx?需要從上層的?cancelCtx移除,調(diào)用removeChild方法 ??//?c.Context?就是自己的父Context ?if?removeFromParent?{ ??removeChild(c.Context,?c) ?} }
從propagateCancel方法中可以看到,只有parent 屬于 cancelCtx 類型 ,才會(huì)將自己掛載。因此 removeChild 會(huì)再次判斷 parent 是否為 cancelCtx,和之前的邏輯保持一致。找到的話,再將自己移除,需要注意的是,移除會(huì)把自己及其自己下面的所有子節(jié)點(diǎn)都移除。
如果上一步 propagateCancel 方法將自己掛載到了 A 上,但是在調(diào)用 cancel() 時(shí),A 已經(jīng)取消過(guò)了,此時(shí) parentCancelCtx() 會(huì)返回 false。不過(guò)這沒(méi)有關(guān)系,A 取消時(shí)已經(jīng)將掛載的子節(jié)點(diǎn)移除了,當(dāng)前的子節(jié)點(diǎn)不用將自己從 A 中移除了。
func?removeChild(parent?Context,?child?canceler)?{ ??//?parent?是否為未取消的?cancelCtx ?p,?ok?:=?parentCancelCtx(parent) ?if?!ok?{ ??return ?} ??//?獲取?parent?cancelCtx?的鎖,修改保護(hù)字段?children ?p.mu.Lock() ??//?將自己從?parent?cancelCtx?的?children?中刪除 ?if?p.children?!=?nil?{ ??delete(p.children,?child) ?} ?p.mu.Unlock() }
parentCancelCtx 判斷 parent 是否為 未取消的 *cancelCtx。取消與否容易判斷,難判斷的是 parent 是否為 *cancelCtx,因?yàn)橛锌赡芷渌Y(jié)構(gòu)體內(nèi)嵌了 cancelCtx,比如 timerCtx,會(huì)通過(guò)比對(duì) channel 來(lái)確定。
func?parentCancelCtx(parent?Context)?(*cancelCtx,?bool)?{ ?? ??//?如果?parent?context?的?done?為?nil,?說(shuō)明不支持?cancel,那么就不可能是?cancelCtx ?//?如果?parent?context?的?done?為?closedchan,?說(shuō)明?parent?context?已經(jīng)?cancel?了 ?done?:=?parent.Done() ?if?done?==?closedchan?||?done?==?nil?{ ??return?nil,?false ?} ?? ??//?到這里說(shuō)明支持取消,且沒(méi)有被取消 ?? ?//?如果?parent?context?屬于原生的?*cancelCtx?或衍生類型,需要繼續(xù)進(jìn)行后續(xù)判斷 ?//?如果?parent?context?無(wú)法轉(zhuǎn)換到?*cancelCtx,則認(rèn)為非?cancelCtx,返回?nil,fasle ?p,?ok?:=?parent.Value(&cancelCtxKey).(*cancelCtx) ?if?!ok?{ ??return?nil,?false ?} ? ??//?經(jīng)過(guò)上面的判斷后,說(shuō)明?parent?context?可以被轉(zhuǎn)換為?*cancelCtx,這時(shí)存在多種情況: ?//???-?parent?context?就是?*cancelCtx ?//???-?parent?context?是標(biāo)準(zhǔn)庫(kù)中的?timerCtx ?//???-?parent?context?是個(gè)自己自定義包裝的?cancelCtx ?// ?//?針對(duì)這?3?種情況需要進(jìn)行判斷,判斷方法就是:? ?//???判斷?parent?context?通過(guò)?Done()?方法獲取的?done?channel?與?Value?查找到的?context?的?done?channel?是否一致 ?//? ?//?一致情況說(shuō)明?parent?context?為?cancelCtx?或?timerCtx?或?自定義的?cancelCtx?且未重寫?Done(), ?//?這種情況下可以認(rèn)為拿到了底層的?*cancelCtx ?//? ?//?不一致情況說(shuō)明?parent?context?是一個(gè)自定義的?cancelCtx?且重寫了?Done()?方法,并且并未返回標(biāo)準(zhǔn)?*cancelCtx?的 ?//?的?done?channel,這種情況需要單獨(dú)處理,故返回?nil,?false ?pdone,?_?:=?p.done.Load().(chan?struct{}) ?if?pdone?!=?done?{ ??return?nil,?false ?} ?return?p,?true }
timerCtx
簡(jiǎn)介
timerCtx 嵌入了 cancelCtx,并新增了一個(gè) timer 和 deadline 字段。timerCtx 的取消能力是復(fù)用 cancelCtx 的,只是在這個(gè)基礎(chǔ)上增加了定時(shí)取消而已。
在我們的使用過(guò)程中,有可能還沒(méi)到 deadline,任務(wù)就提前完成了,此時(shí)需要手動(dòng)調(diào)用 CancelFunc。
func?slowOperationWithTimeout(ctx?context.Context)?(Result,?error)?{ ??ctx,?cancel?:=?context.WithTimeout(ctx,?100*time.Millisecond) ????defer?cancel()??//?如果未到截止時(shí)間,slowOperation就完成了,盡早調(diào)用?cancel()?釋放資源 ??return?slowOperation(ctx) }
類型定義
type?timerCtx?struct?{ ???cancelCtx?//?內(nèi)嵌?cancelCtx ???timer?*time.Timer?//?受?cancelCtx.mu?互斥鎖的保護(hù) ???deadline?time.Time?//?截止時(shí)間 }
Deadline() 返回 deadline 字段的值
func?(c?*timerCtx)?Deadline()?(deadline?time.Time,?ok?bool)?{ ?return?c.deadline,?true }
WithDeadline
WithDeadline 基于parent Context 和 時(shí)間點(diǎn) d,返回了一個(gè)定時(shí)取消的 Context,以及一個(gè) CancelFunc。返回的Context 有三種情況被取消:1. 到達(dá)了指定時(shí)間,就會(huì)主動(dòng)取消;2. 手動(dòng)調(diào)用了 CancelFunc;3. 父Context取消,導(dǎo)致該Context被取消。這三種情況哪種先到,就會(huì)首次觸發(fā)取消操作,后續(xù)的再次取消不會(huì)產(chǎn)生任何效果。
如果傳入 parent Context 的 deadline 比指定的時(shí)間 d 還要早,此時(shí) d 就沒(méi)用處了,直接依賴 parent 取消傳播就可以了。
func?WithDeadline(parent?Context,?d?time.Time)?(Context,?CancelFunc)?{ ?? ??//?傳入的?parent?不能為?nil ?if?parent?==?nil?{ ??panic("cannot?create?context?from?nil?parent") ?} ?? ??//?parent?也有?deadline,并且比?d?還要早,直接依賴?parent?的取消傳播即可 ?if?cur,?ok?:=?parent.Deadline();?ok?&&?cur.Before(d)?{ ??//?The?current?deadline?is?already?sooner?than?the?new?one. ??return?WithCancel(parent) ?} ?? ??//?定義?timerCtx?接口 ?c?:=?&timerCtx{ ??cancelCtx:?newCancelCtx(parent), ??deadline:??d, ?} ?? ??//?設(shè)置傳播,如果parent?屬于?cancelCtx,會(huì)掛載到?children?字段上 ?propagateCancel(parent,?c) ?? ??//?距離截止時(shí)間?d?還有多久 ?dur?:=?time.Until(d) ?if?dur?<=?0?{ ????//?已經(jīng)到了截止時(shí)間,直接取消,同時(shí)從?parent?中取消掛載 ????//?由于是超時(shí),取消時(shí)的?err?是?DeadlineExceeded ??c.cancel(true,?DeadlineExceeded)? ???? ????//?再返回?c?和?CancelFunc,已經(jīng)取消掛載了,此時(shí)的?CancelFunc?不會(huì)從?parent?中取消掛載 ????//?后面再次調(diào)用?CancelFunc?不會(huì)產(chǎn)生任何效果了 ????//?主動(dòng)取消的話,err?是?Canceled ??return?c,?func()?{?c.cancel(false,?Canceled)?} ?} ?? ??//?還沒(méi)有到截止時(shí)間,定義一個(gè)定時(shí)器,過(guò)了?dur?會(huì)自動(dòng)取消 ?c.mu.Lock() ?defer?c.mu.Unlock() ?if?c.err?==?nil?{ ??c.timer?=?time.AfterFunc(dur,?func()?{ ??????//?由于是到了截止時(shí)間才取消,err?是?DeadlineExceeded ???c.cancel(true,?DeadlineExceeded) ??}) ?} ?? ??//?返回?c?和?cancelFunc,主動(dòng)取消的?err?是?Canceled ?return?c,?func()?{?c.cancel(true,?Canceled)?} }
接下來(lái)我們看下 cancel 方法,timerCtx 的 cancel 方法 就是調(diào)用內(nèi)嵌 cancelCtx 的 cancel() 方法,默認(rèn)是不從父節(jié)點(diǎn)移除
func?(c?*timerCtx)?cancel(removeFromParent?bool,?err?error)?{ ?c.cancelCtx.cancel(false,?err) ?? ??//?從父節(jié)點(diǎn)中移除 ?if?removeFromParent?{ ??removeChild(c.cancelCtx.Context,?c) ?} ?? ??//?把定時(shí)器停了,釋放資源 ??//?有可能還沒(méi)到deadline,手動(dòng)觸發(fā)了?CancelFunc,此時(shí)把?timer?停了 ?c.mu.Lock() ?if?c.timer?!=?nil?{ ??c.timer.Stop() ??c.timer?=?nil ?} ?c.mu.Unlock() }
WithTimeout
WithTimeout 就是基于 WithDeadline,deadline 就是基于當(dāng)前時(shí)間計(jì)算的
func?WithTimeout(parent?Context,?timeout?time.Duration)?(Context,?CancelFunc)?{ ?return?WithDeadline(parent,?time.Now().Add(timeout)) }
總結(jié)
本篇文章,我們通過(guò)源碼+示例的方式,一起學(xué)習(xí)了 context 包相關(guān)的結(jié)構(gòu)以及實(shí)現(xiàn)邏輯,包括如下內(nèi)容
Context 接口:定義了一些接口方法和規(guī)范
emptyCtx:空的Context,Background() 和 TODO() 方法就是使用的 emptyCtx
valueCtx:用于保存鍵值對(duì),查詢時(shí)是遞歸查詢,可以用于 LogID 這種全局 id 的保存
cancelCtx:可以取消的Context,用于取消信號(hào)的傳遞
timerCtx:定時(shí)取消的 cancelCtx
以上就是Go語(yǔ)言學(xué)習(xí)之context包的用法詳解的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言 context包的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
模塊一 GO語(yǔ)言基礎(chǔ)知識(shí)-庫(kù)源碼文件
這篇文章主要介紹了模塊一 GO語(yǔ)言基礎(chǔ)知識(shí)-庫(kù)源碼文件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01Go?處理大數(shù)組使用?for?range?和?for?循環(huán)的區(qū)別
這篇文章主要介紹了Go處理大數(shù)組使用for?range和for循環(huán)的區(qū)別,對(duì)于遍歷大數(shù)組而言,for循環(huán)能比f(wàn)or?range循環(huán)更高效與穩(wěn)定,這一點(diǎn)在數(shù)組元素為結(jié)構(gòu)體類型更加明顯,下文具體分析感興趣得小伙伴可以參考一下2022-05-05Go語(yǔ)言實(shí)現(xiàn)LRU算法的核心思想和實(shí)現(xiàn)過(guò)程
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)LRU算法的核心思想和實(shí)現(xiàn)過(guò)程,LRU算法是一種常用的緩存淘汰策略,它的核心思想是如果一個(gè)數(shù)據(jù)在最近一段時(shí)間內(nèi)沒(méi)有被訪問(wèn)到,那么在將來(lái)它被訪問(wèn)的可能性也很小,因此可以將其淘汰,感興趣想要詳細(xì)了解可以參考下文2023-05-05