小白也能看懂的 Context 包詳解:從入門(mén)到精通
前言
哈嘍,大家好,我是asong。今天想與大家分享context包,經(jīng)過(guò)一年的沉淀,重新出發(fā),基于Go1.17.1從源碼角度再次分析,不過(guò)這次不同的是,我打算先從入門(mén)開(kāi)始,因?yàn)榇蠖鄶?shù)初學(xué)的讀者都想先知道怎么用,然后才會(huì)關(guān)心源碼是如何實(shí)現(xiàn)的。
相信大家在日常工作開(kāi)發(fā)中一定會(huì)看到這樣的代碼:
- func a1(ctx context ...){
 - b1(ctx)
 - }
 - func b1(ctx context ...){
 - c1(ctx)
 - }
 - func c1(ctx context ...)
 
context被當(dāng)作第一個(gè)參數(shù)(官方建議),并且不斷透?jìng)飨氯?,基本一個(gè)項(xiàng)目代碼中到處都是context,但是你們真的知道它有何作用嗎以及它是如何起作用的嗎?我記得我第一次接觸context時(shí),同事都說(shuō)這個(gè)用來(lái)做并發(fā)控制的,可以設(shè)置超時(shí)時(shí)間,超時(shí)就會(huì)取消往下執(zhí)行,快速返回,我就單純的認(rèn)為只要函數(shù)中帶著context參數(shù)往下傳遞就可以做到超時(shí)取消,快速返回。相信大多數(shù)初學(xué)者也都是和我一個(gè)想法,其實(shí)這是一個(gè)錯(cuò)誤的思想,其取消機(jī)制采用的也是通知機(jī)制,單純的透?jìng)鞑⒉粫?huì)起作用,比如你這樣寫(xiě)代碼:
- func main() {
 - ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
 - defer cancel()
 - go Monitor(ctx)
 - time.Sleep(20 * time.Second)
 - }
 - func Monitor(ctx context.Context) {
 - for {
 - fmt.Print("monitor")
 - }
 - }
 
即使context透?jìng)飨氯チ?,沒(méi)有監(jiān)聽(tīng)取消信號(hào)也是不起任何作用的。所以了解context的使用還是很有必要的,本文就先從使用開(kāi)始,逐步解析Go語(yǔ)言的context包,下面我們就開(kāi)始嘍!!!
context包的起源與作用
看官方博客我們可以知道context包是在go1.7版本中引入到標(biāo)準(zhǔn)庫(kù)中的:
context可以用來(lái)在goroutine之間傳遞上下文信息,相同的context可以傳遞給運(yùn)行在不同goroutine中的函數(shù),上下文對(duì)于多個(gè)goroutine同時(shí)使用是安全的,context包定義了上下文類(lèi)型,可以使用background、TODO創(chuàng)建一個(gè)上下文,在函數(shù)調(diào)用鏈之間傳播context,也可以使用WithDeadline、WithTimeout、WithCancel 或 WithValue 創(chuàng)建的修改副本替換它,聽(tīng)起來(lái)有點(diǎn)繞,其實(shí)總結(jié)起就是一句話(huà):context的作用就是在不同的goroutine之間同步請(qǐng)求特定的數(shù)據(jù)、取消信號(hào)以及處理請(qǐng)求的截止日期。
目前我們常用的一些庫(kù)都是支持context的,例如gin、database/sql等庫(kù)都是支持context的,這樣更方便我們做并發(fā)控制了,只要在服務(wù)器入口創(chuàng)建一個(gè)context上下文,不斷透?jìng)飨氯ゼ纯伞?/p>
context的使用
創(chuàng)建context
context包主要提供了兩種方式創(chuàng)建context:
- context.Backgroud()
 - context.TODO()
 
這兩個(gè)函數(shù)其實(shí)只是互為別名,沒(méi)有差別,官方給的定義是:
- context.Background 是上下文的默認(rèn)值,所有其他的上下文都應(yīng)該從它衍生(Derived)出來(lái)。
 - context.TODO 應(yīng)該只在不確定應(yīng)該使用哪種上下文時(shí)使用;
 
所以在大多數(shù)情況下,我們都使用context.Background作為起始的上下文向下傳遞。
上面的兩種方式是創(chuàng)建根context,不具備任何功能,具體實(shí)踐還是要依靠context包提供的With系列函數(shù)來(lái)進(jìn)行派生:
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
 - func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
 - func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
 - func WithValue(parent Context, key, val interface{}) Context
 
這四個(gè)函數(shù)都要基于父Context衍生,通過(guò)這些函數(shù),就創(chuàng)建了一顆Context樹(shù),樹(shù)的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)層級(jí)可以有任意多個(gè),畫(huà)個(gè)圖表示一下:
基于一個(gè)父Context可以隨意衍生,其實(shí)這就是一個(gè)Context樹(shù),樹(shù)的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)層級(jí)可以有任意多個(gè),每個(gè)子節(jié)點(diǎn)都依賴(lài)于其父節(jié)點(diǎn),例如上圖,我們可以基于Context.Background衍生出四個(gè)子context:ctx1.0-cancel、ctx2.0-deadline、ctx3.0-timeout、ctx4.0-withvalue,這四個(gè)子context還可以作為父context繼續(xù)向下衍生,即使其中ctx1.0-cancel 節(jié)點(diǎn)取消了,也不影響其他三個(gè)父節(jié)點(diǎn)分支。
創(chuàng)建context方法和context的衍生方法就這些,下面我們就一個(gè)一個(gè)來(lái)看一下他們?nèi)绾伪皇褂谩?/p>
WithValue攜帶數(shù)據(jù)
我們?nèi)粘T跇I(yè)務(wù)開(kāi)發(fā)中都希望能有一個(gè)trace_id能串聯(lián)所有的日志,這就需要我們打印日志時(shí)能夠獲取到這個(gè)trace_id,在python中我們可以用gevent.local來(lái)傳遞,在java中我們可以用ThreadLocal來(lái)傳遞,在Go語(yǔ)言中我們就可以使用Context來(lái)傳遞,通過(guò)使用WithValue來(lái)創(chuàng)建一個(gè)攜帶trace_id的context,然后不斷透?jìng)飨氯?,打印日志時(shí)輸出即可,來(lái)看使用例子:
- const (
 - KEY = "trace_id"
 - )
 - func NewRequestID() string {
 - return strings.Replace(uuid.New().String(), "-", "", -1)
 - }
 - func NewContextWithTraceID() context.Context {
 - ctx := context.WithValue(context.Background(), KEY,NewRequestID())
 - return ctx
 - }
 - func PrintLog(ctx context.Context, message string) {
 - fmt.Printf("%s|info|trace_id=%s|%s",time.Now().Format("2006-01-02 15:04:05") , GetContextValue(ctx, KEY), message)
 - }
 - func GetContextValue(ctx context.Context,k string) string{
 - v, ok := ctx.Value(k).(string)
 - if !ok{
 - return ""
 - }
 - return v
 - }
 - func ProcessEnter(ctx context.Context) {
 - PrintLog(ctx, "Golang夢(mèng)工廠(chǎng)")
 - }
 - func main() {
 - ProcessEnter(NewContextWithTraceID())
 - }
 
輸出結(jié)果:
- 2021-10-31 15:13:25|info|trace_id=7572e295351e478e91b1ba0fc37886c0|Golang夢(mèng)工廠(chǎng)
 - Process finished with the exit code 0
 
我們基于context.Background創(chuàng)建一個(gè)攜帶trace_id的ctx,然后通過(guò)context樹(shù)一起傳遞,從中派生的任何context都會(huì)獲取此值,我們最后打印日志的時(shí)候就可以從ctx中取值輸出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下傳遞就更方便了。
在使用withVaule時(shí)要注意四個(gè)事項(xiàng):
- 不建議使用context值傳遞關(guān)鍵參數(shù),關(guān)鍵參數(shù)應(yīng)該顯示的聲明出來(lái),不應(yīng)該隱式處理,context中最好是攜帶簽名、trace_id這類(lèi)值。
 - 因?yàn)閿y帶value也是key、value的形式,為了避免context因多個(gè)包同時(shí)使用context而帶來(lái)沖突,key建議采用內(nèi)置類(lèi)型。
 - 上面的例子我們獲取trace_id是直接從當(dāng)前ctx獲取的,實(shí)際我們也可以獲取父context中的value,在獲取鍵值對(duì)是,我們先從當(dāng)前context中查找,沒(méi)有找到會(huì)在從父context中查找該鍵對(duì)應(yīng)的值直到在某個(gè)父context中返回 nil 或者查找到對(duì)應(yīng)的值。
 - context傳遞的數(shù)據(jù)中key、value都是interface類(lèi)型,這種類(lèi)型編譯期無(wú)法確定類(lèi)型,所以不是很安全,所以在類(lèi)型斷言時(shí)別忘了保證程序的健壯性。
 
超時(shí)控制
通常健壯的程序都是要設(shè)置超時(shí)時(shí)間的,避免因?yàn)榉?wù)端長(zhǎng)時(shí)間響應(yīng)消耗資源,所以一些web框架或rpc框架都會(huì)采用withTimeout或者withDeadline來(lái)做超時(shí)控制,當(dāng)一次請(qǐng)求到達(dá)我們?cè)O(shè)置的超時(shí)時(shí)間,就會(huì)及時(shí)取消,不在往下執(zhí)行。withTimeout和withDeadline作用是一樣的,就是傳遞的時(shí)間參數(shù)不同而已,他們都會(huì)通過(guò)傳入的時(shí)間來(lái)自動(dòng)取消Context,這里要注意的是他們都會(huì)返回一個(gè)cancelFunc方法,通過(guò)調(diào)用這個(gè)方法可以達(dá)到提前進(jìn)行取消,不過(guò)在使用的過(guò)程還是建議在自動(dòng)取消后也調(diào)用cancelFunc去停止定時(shí)減少不必要的資源浪費(fèi)。
withTimeout、WithDeadline不同在于WithTimeout將持續(xù)時(shí)間作為參數(shù)輸入而不是時(shí)間對(duì)象,這兩個(gè)方法使用哪個(gè)都是一樣的,看業(yè)務(wù)場(chǎng)景和個(gè)人習(xí)慣了,因?yàn)楸举|(zhì)withTimout內(nèi)部也是調(diào)用的WithDeadline。
現(xiàn)在我們就舉個(gè)例子來(lái)試用一下超時(shí)控制,現(xiàn)在我們就模擬一個(gè)請(qǐng)求寫(xiě)兩個(gè)例子:
- 達(dá)到超時(shí)時(shí)間終止接下來(lái)的執(zhí)行
 
- func main() {
 - HttpHandler()
 - }
 - func NewContextWithTimeout() (context.Context,context.CancelFunc) {
 - return context.WithTimeout(context.Background(), 3 * time.Second)
 - }
 - func HttpHandler() {
 - ctx, cancel := NewContextWithTimeout()
 - defer cancel()
 - deal(ctx)
 - }
 - func deal(ctx context.Context) {
 - for i:=0; i< 10; i++ {
 - time.Sleep(1*time.Second)
 - select {
 - case <- ctx.Done():
 - fmt.Println(ctx.Err())
 - return
 - default:
 - fmt.Printf("deal time is %d\n", i)
 - }
 - }
 - }
 
輸出結(jié)果:
- deal time is 0
 - deal time is 1
 - context deadline exceeded
 
- 沒(méi)有達(dá)到超時(shí)時(shí)間終止接下來(lái)的執(zhí)行
 
- func main() {
 - HttpHandler1()
 - }
 - func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
 - return context.WithTimeout(context.Background(), 3 * time.Second)
 - }
 - func HttpHandler1() {
 - ctx, cancel := NewContextWithTimeout1()
 - defer cancel()
 - deal1(ctx, cancel)
 - }
 - func deal1(ctx context.Context, cancel context.CancelFunc) {
 - for i:=0; i< 10; i++ {
 - time.Sleep(1*time.Second)
 - select {
 - case <- ctx.Done():
 - fmt.Println(ctx.Err())
 - return
 - default:
 - fmt.Printf("deal time is %d\n", i)
 - cancel()
 - }
 - }
 - }
 
輸出結(jié)果:
- deal time is 0
 - context canceled
 
使用起來(lái)還是比較容易的,既可以超時(shí)自動(dòng)取消,又可以手動(dòng)控制取消。這里大家要記的一個(gè)坑,就是我們往從請(qǐng)求入口透?jìng)鞯恼{(diào)用鏈路中的context是攜帶超時(shí)時(shí)間的,如果我們想在其中單獨(dú)開(kāi)一個(gè)goroutine去處理其他的事情并且不會(huì)隨著請(qǐng)求結(jié)束后而被取消的話(huà),那么傳遞的context要基于context.Background或者context.TODO重新衍生一個(gè)傳遞,否決就會(huì)和預(yù)期不符合了,可以看一下我之前的一篇踩坑文章:context使用不當(dāng)引發(fā)的一個(gè)bug。
withCancel取消控制
日常業(yè)務(wù)開(kāi)發(fā)中我們往往為了完成一個(gè)復(fù)雜的需求會(huì)開(kāi)多個(gè)gouroutine去做一些事情,這就導(dǎo)致我們會(huì)在一次請(qǐng)求中開(kāi)了多個(gè)goroutine確無(wú)法控制他們,這時(shí)我們就可以使用withCancel來(lái)衍生一個(gè)context傳遞到不同的goroutine中,當(dāng)我想讓這些goroutine停止運(yùn)行,就可以調(diào)用cancel來(lái)進(jìn)行取消。
來(lái)看一個(gè)例子:
- func main() {
 - ctx,cancel := context.WithCancel(context.Background())
 - go Speak(ctx)
 - time.Sleep(10*time.Second)
 - cancel()
 - time.Sleep(1*time.Second)
 - }
 - func Speak(ctx context.Context) {
 - for range time.Tick(time.Second){
 - select {
 - case <- ctx.Done():
 - fmt.Println("我要閉嘴了")
 - return
 - default:
 - fmt.Println("balabalabalabala")
 - }
 - }
 - }
 
運(yùn)行結(jié)果:
- balabalabalabala
 - ....省略
 - balabalabalabala
 - 我要閉嘴了
 
我們使用withCancel創(chuàng)建一個(gè)基于Background的ctx,然后啟動(dòng)一個(gè)講話(huà)程序,每隔1s說(shuō)一話(huà),main函數(shù)在10s后執(zhí)行cancel,那么speak檢測(cè)到取消信號(hào)就會(huì)退出。
自定義Context
因?yàn)镃ontext本質(zhì)是一個(gè)接口,所以我們可以通過(guò)實(shí)現(xiàn)Context達(dá)到自定義Context的目的,一般在實(shí)現(xiàn)Web框架或RPC框架往往采用這種形式,比如gin框架的Context就是自己有封裝了一層,具體代碼和實(shí)現(xiàn)就貼在這里,有興趣可以看一下gin.Context是如何實(shí)現(xiàn)的。
源碼賞析
Context其實(shí)就是一個(gè)接口,定義了四個(gè)方法:
- type Context interface {
 - Deadline() (deadline time.Time, ok bool)
 - Done() <-chan struct{}
 - Err() error
 - Value(key interface{}) interface{}
 - }
 
- Deadlne方法:當(dāng)Context自動(dòng)取消或者到了取消時(shí)間被取消后返回
 - Done方法:當(dāng)Context被取消或者到了deadline返回一個(gè)被關(guān)閉的channel
 - Err方法:當(dāng)Context被取消或者關(guān)閉后,返回context取消的原因
 - Value方法:獲取設(shè)置的key對(duì)應(yīng)的值
 
這個(gè)接口主要被三個(gè)類(lèi)繼承實(shí)現(xiàn),分別是emptyCtx、ValueCtx、cancelCtx,采用匿名接口的寫(xiě)法,這樣可以對(duì)任意實(shí)現(xiàn)了該接口的類(lèi)型進(jìn)行重寫(xiě)。
下面我們就從創(chuàng)建到使用來(lái)層層分析。
創(chuàng)建根Context
其在我們調(diào)用context.Background、context.TODO時(shí)創(chuàng)建的對(duì)象就是empty:
- var (
 - background = new(emptyCtx)
 - todo = new(emptyCtx)
 - )
 - func Background() Context {
 - return background
 - }
 - func TODO() Context {
 - return todo
 - }
 
Background和TODO還是一模一樣的,官方說(shuō):background它通常由主函數(shù)、初始化和測(cè)試使用,并作為傳入請(qǐng)求的頂級(jí)上下文;TODO是當(dāng)不清楚要使用哪個(gè) Context 或尚不可用時(shí),代碼應(yīng)使用 context.TODO,后續(xù)在在進(jìn)行替換掉,歸根結(jié)底就是語(yǔ)義不同而已。
emptyCtx類(lèi)
emptyCtx主要是給我們創(chuàng)建根Context時(shí)使用的,其實(shí)現(xiàn)方法也是一個(gè)空結(jié)構(gòu),實(shí)際源代碼長(zhǎng)這樣:
- 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"
 - }
 
WithValue的實(shí)現(xiàn)
withValue內(nèi)部主要就是調(diào)用valueCtx類(lèi):
- 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類(lèi)
valueCtx目的就是為Context攜帶鍵值對(duì),因?yàn)樗捎媚涿涌诘睦^承實(shí)現(xiàn)方式,他會(huì)繼承父Context,也就相當(dāng)于嵌入Context當(dāng)中了
- type valueCtx struct {
 - Context
 - key, val interface{}
 - }
 
實(shí)現(xiàn)了String方法輸出Context和攜帶的鍵值對(duì)信息:
- func (c *valueCtx) String() string {
 - return contextName(c.Context) + ".WithValue(type " +
 - reflectlite.TypeOf(c.key).String() +
 - ", val " + stringify(c.val) + ")"
 - }
 
實(shí)現(xiàn)Value方法來(lái)存儲(chǔ)鍵值對(duì):
- func (c *valueCtx) Value(key interface{}) interface{} {
 - if c.key == key {
 - return c.val
 - }
 - return c.Context.Value(key)
 - }
 
看圖來(lái)理解一下:
所以我們?cè)谡{(diào)用Context中的Value方法時(shí)會(huì)層層向上調(diào)用直到最終的根節(jié)點(diǎn),中間要是找到了key就會(huì)返回,否會(huì)就會(huì)找到最終的emptyCtx返回nil。
WithCancel的實(shí)現(xiàn)
我們來(lái)看一下WithCancel的入口函數(shù)源代碼:
- 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) }
 - }
 
這個(gè)函數(shù)執(zhí)行步驟如下:
- 創(chuàng)建一個(gè)cancelCtx對(duì)象,作為子context
 - 然后調(diào)用propagateCancel構(gòu)建父子context之間的關(guān)聯(lián)關(guān)系,這樣當(dāng)父context被取消時(shí),子context也會(huì)被取消。
 - 返回子context對(duì)象和子樹(shù)取消函數(shù)
 
我們先分析一下cancelCtx這個(gè)類(lèi)。
cancelCtx類(lèi)
cancelCtx繼承了Context,也實(shí)現(xiàn)了接口canceler:
- type cancelCtx struct {
 - Context
 - mu sync.Mutex // protects following fields
 - done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
 - children map[canceler]struct{} // set to nil by the first cancel call
 - err error // set to non-nil by the first cancel call
 - }
 
字短解釋?zhuān)?/p>
- mu:就是一個(gè)互斥鎖,保證并發(fā)安全的,所以context是并發(fā)安全的
 - done:用來(lái)做context的取消通知信號(hào),之前的版本使用的是chan struct{}類(lèi)型,現(xiàn)在用atomic.Value做鎖優(yōu)化
 - children:key是接口類(lèi)型canceler,目的就是存儲(chǔ)實(shí)現(xiàn)當(dāng)前canceler接口的子節(jié)點(diǎn),當(dāng)根節(jié)點(diǎn)發(fā)生取消時(shí),遍歷子節(jié)點(diǎn)發(fā)送取消信號(hào)
 - error:當(dāng)context取消時(shí)存儲(chǔ)取消信息
 
這里實(shí)現(xiàn)了Done方法,返回的是一個(gè)只讀的channel,目的就是我們?cè)谕獠靠梢酝ㄟ^(guò)這個(gè)阻塞的channel等待通知信號(hào)。
具體代碼就不貼了。我們先返回去看propagateCancel是如何做構(gòu)建父子Context之間的關(guān)聯(lián)。
propagateCancel方法
代碼有點(diǎn)長(zhǎng),解釋有點(diǎn)麻煩,我把注釋添加到代碼中看起來(lái)比較直觀(guān):
- func propagateCancel(parent Context, child canceler) {
 - // 如果返回nil,說(shuō)明當(dāng)前父`context`從來(lái)不會(huì)被取消,是一個(gè)空節(jié)點(diǎn),直接返回即可。
 - done := parent.Done()
 - if done == nil {
 - return // parent is never canceled
 - }
 - // 提前判斷一個(gè)父context是否被取消,如果取消了也不需要構(gòu)建關(guān)聯(lián)了,
 - // 把當(dāng)前子節(jié)點(diǎn)取消掉并返回
 - select {
 - case <-done:
 - // parent is already canceled
 - child.cancel(false, parent.Err())
 - return
 - default:
 - }
 - // 這里目的就是找到可以“掛”、“取消”的context
 - if p, ok := parentCancelCtx(parent); ok {
 - p.mu.Lock()
 - // 找到了可以“掛”、“取消”的context,但是已經(jīng)被取消了,那么這個(gè)子節(jié)點(diǎn)也不需要
 - // 繼續(xù)掛靠了,取消即可
 - if p.err != nil {
 - child.cancel(false, p.err)
 - } else {
 - // 將當(dāng)前節(jié)點(diǎn)掛到父節(jié)點(diǎn)的childrn map中,外面調(diào)用cancel時(shí)可以層層取消
 - if p.children == nil {
 - // 這里因?yàn)閏hilder節(jié)點(diǎn)也會(huì)變成父節(jié)點(diǎn),所以需要初始化map結(jié)構(gòu)
 - p.children = make(map[canceler]struct{})
 - }
 - p.children[child] = struct{}{}
 - }
 - p.mu.Unlock()
 - } else {
 - // 沒(méi)有找到可“掛”,“取消”的父節(jié)點(diǎn)掛載,那么就開(kāi)一個(gè)goroutine
 - atomic.AddInt32(&goroutines, +1)
 - go func() {
 - select {
 - case <-parent.Done():
 - child.cancel(false, parent.Err())
 - case <-child.Done():
 - }
 - }()
 - }
 - }
 
這段代碼真正產(chǎn)生疑惑的是這個(gè)if、else分支。不看代碼了,直接說(shuō)為什么吧。因?yàn)槲覀兛梢宰约憾ㄖ芻ontext,把context塞進(jìn)一個(gè)結(jié)構(gòu)時(shí),就會(huì)導(dǎo)致找不到可取消的父節(jié)點(diǎn),只能重新起一個(gè)協(xié)程做監(jiān)聽(tīng)。
對(duì)這塊有迷惑的推薦閱讀饒大大文章:[深度解密Go語(yǔ)言之context](https://www.cnblogs.com/qcrao-2018/p/11007503.html),定能為你排憂(yōu)解惑。
cancel方法
最后我們?cè)賮?lái)看一下返回的cancel方法是如何實(shí)現(xiàn),這個(gè)方法會(huì)關(guān)閉上下文中的 Channel 并向所有的子上下文同步取消信號(hào):
- func (c *cancelCtx) cancel(removeFromParent bool, err error) {
 - // 取消時(shí)傳入的error信息不能為nil, context定義了默認(rèn)error:var Canceled = errors.New("context canceled")
 - if err == nil {
 - panic("context: internal error: missing cancel error")
 - }
 - // 已經(jīng)有錯(cuò)誤信息了,說(shuō)明當(dāng)前節(jié)點(diǎn)已經(jīng)被取消過(guò)了
 - c.mu.Lock()
 - if c.err != nil {
 - c.mu.Unlock()
 - return // already canceled
 - }
 - c.err = err
 - // 用來(lái)關(guān)閉channel,通知其他協(xié)程
 - d, _ := c.done.Load().(chan struct{})
 - if d == nil {
 - c.done.Store(closedchan)
 - } else {
 - close(d)
 - }
 - // 當(dāng)前節(jié)點(diǎn)向下取消,遍歷它的所有子節(jié)點(diǎn),然后取消
 - for child := range c.children {
 - // NOTE: acquiring the child's lock while holding parent's lock.
 - child.cancel(false, err)
 - }
 - // 節(jié)點(diǎn)置空
 - c.children = nil
 - c.mu.Unlock()
 - // 把當(dāng)前節(jié)點(diǎn)從父節(jié)點(diǎn)中移除,只有在外部父節(jié)點(diǎn)調(diào)用時(shí)才會(huì)傳true
 - // 其他都是傳false,內(nèi)部調(diào)用都會(huì)因?yàn)閏.children = nil被剔除出去
 - if removeFromParent {
 - removeChild(c.Context, c)
 - }
 - }
 
到這里整個(gè)WithCancel方法源碼就分析好了,通過(guò)源碼我們可以知道cancel方法可以被重復(fù)調(diào)用,是冪等的。
withDeadline、WithTimeout的實(shí)現(xiàn)
先看WithTimeout方法,它內(nèi)部就是調(diào)用的WithDeadline方法:
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
 - return WithDeadline(parent, time.Now().Add(timeout))
 - }
 
所以我們重點(diǎn)來(lái)看withDeadline是如何實(shí)現(xiàn)的:
- func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 - // 不能為空`context`創(chuàng)建衍生context
 - if parent == nil {
 - panic("cannot create context from nil parent")
 - }
 - // 當(dāng)父context的結(jié)束時(shí)間早于要設(shè)置的時(shí)間,則不需要再去單獨(dú)處理子節(jié)點(diǎn)的定時(shí)器了
 - if cur, ok := parent.Deadline(); ok && cur.Before(d) {
 - // The current deadline is already sooner than the new one.
 - return WithCancel(parent)
 - }
 - // 創(chuàng)建一個(gè)timerCtx對(duì)象
 - c := &timerCtx{
 - cancelCtx: newCancelCtx(parent),
 - deadline: d,
 - }
 - // 將當(dāng)前節(jié)點(diǎn)掛到父節(jié)點(diǎn)上
 - propagateCancel(parent, c)
 - // 獲取過(guò)期時(shí)間
 - dur := time.Until(d)
 - // 當(dāng)前時(shí)間已經(jīng)過(guò)期了則直接取消
 - 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()
 - // 如果沒(méi)被取消,則直接添加一個(gè)定時(shí)器,定時(shí)去取消
 - if c.err == nil {
 - c.timer = time.AfterFunc(dur, func() {
 - c.cancel(true, DeadlineExceeded)
 - })
 - }
 - return c, func() { c.cancel(true, Canceled) }
 - }
 
withDeadline相較于withCancel方法也就多了一個(gè)定時(shí)器去定時(shí)調(diào)用cancel方法,這個(gè)cancel方法在timerCtx類(lèi)中進(jìn)行了重寫(xiě),我們先來(lái)看一下timerCtx類(lèi),他是基于cancelCtx的,多了兩個(gè)字段:
- type timerCtx struct {
 - cancelCtx
 - timer *time.Timer // Under cancelCtx.mu.
 - deadline time.Time
 - }
 
timerCtx實(shí)現(xiàn)的cancel方法,內(nèi)部也是調(diào)用了cancelCtx的cancel方法取消:
- func (c *timerCtx) cancel(removeFromParent bool, err error) {
 - // 調(diào)用cancelCtx的cancel方法取消掉子節(jié)點(diǎn)context
 - c.cancelCtx.cancel(false, err)
 - // 從父context移除放到了這里來(lái)做
 - if removeFromParent {
 - // Remove this timerCtx from its parent cancelCtx's children.
 - removeChild(c.cancelCtx.Context, c)
 - }
 - // 停掉定時(shí)器,釋放資源
 - c.mu.Lock()
 - if c.timer != nil {
 - c.timer.Stop()
 - c.timer = nil
 - }
 - c.mu.Unlock()
 - }
 
終于源碼部分我們就看完了,現(xiàn)在你何感想?
context的優(yōu)缺點(diǎn)
context包被設(shè)計(jì)出來(lái)就是做并發(fā)控制的,這個(gè)包有利有弊,個(gè)人總結(jié)了幾個(gè)優(yōu)缺點(diǎn),歡迎評(píng)論區(qū)補(bǔ)充。
缺點(diǎn)
- 影響代碼美觀(guān),現(xiàn)在基本所有web框架、RPC框架都是實(shí)現(xiàn)了context,這就導(dǎo)致我們的代碼中每一個(gè)函數(shù)的一個(gè)參數(shù)都是context,即使不用也要帶著這個(gè)參數(shù)透?jìng)飨氯?,個(gè)人覺(jué)得有點(diǎn)丑陋。
 - context可以攜帶值,但是沒(méi)有任何限制,類(lèi)型和大小都沒(méi)有限制,也就是沒(méi)有任何約束,這樣很容易導(dǎo)致濫用,程序的健壯很難保證;還有一個(gè)問(wèn)題就是通過(guò)context攜帶值不如顯式傳值舒服,可讀性變差了。
 - 可以自定義context,這樣風(fēng)險(xiǎn)不可控,更加會(huì)導(dǎo)致濫用。
 - context取消和自動(dòng)取消的錯(cuò)誤返回不夠友好,無(wú)法自定義錯(cuò)誤,出現(xiàn)難以排查的問(wèn)題時(shí)不好排查。
 - 創(chuàng)建衍生節(jié)點(diǎn)實(shí)際是創(chuàng)建一個(gè)個(gè)鏈表節(jié)點(diǎn),其時(shí)間復(fù)雜度為O(n),節(jié)點(diǎn)多了會(huì)掉支效率變低。
 
優(yōu)點(diǎn)
使用context可以更好的做并發(fā)控制,能更好的管理goroutine濫用。
context的攜帶者功能沒(méi)有任何限制,這樣我我們傳遞任何的數(shù)據(jù),可以說(shuō)這是一把雙刃劍
網(wǎng)上都說(shuō)context包解決了goroutine的cancelation問(wèn)題,你覺(jué)得呢?
參考文章
https://pkg.go.dev/context@go1.7beta1#Background https://studygolang.com/articles/21531 https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/ https://www.cnblogs.com/qcrao-2018/p/11007503.html https://segmentfault.com/a/1190000039294140 https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
總結(jié)
context雖然在使用上丑陋了一點(diǎn),但是他卻能解決很多問(wèn)題,日常業(yè)務(wù)開(kāi)發(fā)中離不開(kāi)context的使用,不過(guò)也別使用錯(cuò)了context,其取消也采用的channel通知,所以代碼中還有要有監(jiān)聽(tīng)代碼來(lái)監(jiān)聽(tīng)取消信號(hào),這點(diǎn)也是經(jīng)常被廣大初學(xué)者容易忽視的一個(gè)點(diǎn)。
文中示例已上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example
好啦,本文到這里就結(jié)束了,我是asong,我們下期見(jiàn)。


















 
 
 












 
 
 
 