10 Go Context 上下文

一缠犀、Context概述

1.緣起

在開發(fā)web服務(wù)應(yīng)用時浇揩,我們知道http啟動的服務(wù)每接收到一個請求是便啟動一個goroutine處理該request寡夹。而每個協(xié)程處理該請求是一般都會啟動多個協(xié)程去處理不同任務(wù)匾嘱,如調(diào)用RPC饺谬、訪問數(shù)據(jù)庫資源嫩海、緩存資源等等冬殃,這些協(xié)程都是為處理同一個request工作的,同時當(dāng)request被取消或者超時的時候叁怪,從這個request處理協(xié)程創(chuàng)建的所有子協(xié)程也應(yīng)該被結(jié)束审葬。此時一個handler就必須對其啟動的子協(xié)程有控制權(quán),在context出現(xiàn)前奕谭,上述那些處理還是很丑陋的涣觉,有些甚至引起全局資源的濫用或者回調(diào)噩夢。context出現(xiàn)后血柳,一切都得到解脫官册,context解決了處理同一生命周期協(xié)程樹的資源管理問題。

2.官方解釋:

Context难捌,翻譯為“上下文”膝宁,context包定義了Context接口類型,其接口簽名方法定義了跨API邊界和進(jìn)程之間的執(zhí)行最后期限根吁、取消信號和其他請求范圍的值员淫。

對服務(wù)器的傳入請求應(yīng)創(chuàng)建Context類型,對服務(wù)器的傳出調(diào)用應(yīng)接受Context击敌。它們之間的函數(shù)調(diào)用鏈必須傳播Context介返,可以選擇將其替換為使用WithCancel()、WithDeadline()愚争、WithTimeout()或WithValue()創(chuàng)建的派生Context映皆。當(dāng)一個context被取消時挤聘,從它派生的所有context也被取消。WithCancel()捅彻、WithDeadline()和WithTimeout()函數(shù)接受上下文(父級)并返回派生上下文(子級)和Cancelfunc组去。調(diào)用Cancelfunc將取消子級及其子孫級,刪除父級對子級的引用步淹,并停止任何關(guān)聯(lián)的計時器从隆。如果不調(diào)用Cancelfunc,則會泄漏子級及其子孫級缭裆,直到父級被取消或計時器觸發(fā)键闺。Go-Vet工具檢查取消功能是否用于所有控制流路徑。

使用Context的程序應(yīng)該遵循這些規(guī)則澈驼,以保持包之間的接口一致辛燥,并允許靜態(tài)分析工具檢查上下文傳播:

//context傳遞的寫法
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
  • 不要將Context存儲在結(jié)構(gòu)類型中;而是將Context顯式傳遞給每個需要它的函數(shù)缝其。文應(yīng)該是第一個參數(shù)挎塌,通常名為ctx:

  • 即使函數(shù)允許,也不要傳遞nil上下文内边。如果不確定要使用哪個上下文榴都,請傳遞context.TODO(),該函數(shù)返回一個可被跟蹤的頂級Context漠其。

  • 只對傳輸進(jìn)程和API的請求范圍數(shù)據(jù)使用Context值嘴高,而不用于向函數(shù)傳遞可選參數(shù)。

  • 同一Context可以傳遞給在不同goroutine中運(yùn)行的函數(shù)和屎;上下文對于多個goroutine同時使用是安全的拴驮。

3.context包解析

我們來看一下Context接口的簽名方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
3.1 Context接口簽名方法解析:
  • Deadline() (deadline time.Time, ok bool)

Deadline方法返回應(yīng)取消代表此上下文完成的工作的時間。

  • 未設(shè)置截止時間時柴信,Deadline方法返回ok==false莹汤。
  • 對Deadline方法的連續(xù)調(diào)用將返回相同的結(jié)果。
  • Done() <-chan struct{}

done返回一個通道颠印,該通道在應(yīng)取消代表此上下文完成的工作時關(guān)閉。如果無法取消此上下文抹竹,則done可能返回nil线罕。對done的連續(xù)調(diào)用返回相同的值。不同的派生Context對done通道關(guān)閉有不同的處理方式:

  • WithCancel()在調(diào)用cancel時安排關(guān)閉done窃判;
  • WithDeadline()在截止時間過期時安排關(guān)閉done钞楼;
  • WithTimeout()在超時結(jié)束時安排關(guān)閉done。
  • Err() error

Err方法在done關(guān)閉后返回非零錯誤值袄琳。
返回值:

  • 如果上下文被取消询件,則返回Canceled燃乍;如果上下文的截止時間已過,則返回DeadLineExceeded宛琅;
  • 沒有為err定義其他值刻蟹。
  • 完成后關(guān)閉,對err的連續(xù)調(diào)用將返回相同的值嘿辟。
  • Value(key interface{}) interface{}
  • 該方法可以讓協(xié)程共享一些數(shù)據(jù)舆瘪,獲得數(shù)據(jù)是協(xié)程安全的。
  • 該方法返回與鍵的上下文關(guān)聯(lián)的值红伦,如果沒有值與鍵關(guān)聯(lián)英古,則返回nil。
  • 對具有相同鍵的值的連續(xù)調(diào)用返回相同的結(jié)果昙读。
    僅對傳輸進(jìn)程和API邊界的請求范圍數(shù)據(jù)使用上下文值召调,而不用于向函數(shù)傳遞可選參數(shù)。
    鍵標(biāo)識上下文中的特定值蛮浑。希望在上下文中存儲值的函數(shù)通常在全局變量中分配一個鍵唠叛,然后使用該鍵作為context.WithValue() 和 Context.Value的參數(shù)。鍵可以是支持相等的任何類型陵吸;包應(yīng)將鍵定義為未排序的類型以避免沖突玻墅。
3.2 頂級Context

context包提供兩種頂級的上下文類型,由工廠方法創(chuàng)建:

(1).func Background() Context

context.Background()返回非零的空上下文壮虫。它從不被取消澳厢,沒有值,也沒有最后期限囚似。它通常由主函數(shù)剩拢、初始化和測試使用,并且作為傳入請求的頂級上下文饶唤。

(2).func TODO() Context

context.TODO()返回非零的空上下文徐伐。當(dāng)不清楚要使用哪個上下文或者它還不可用時(因為周圍的函數(shù)還沒有被擴(kuò)展以接受上下文參數(shù)),應(yīng)該使用context.TODO()募狂。靜態(tài)分析工具可以識別TODO办素,它確定上下文是否在程序中正確傳播。

兩者區(qū)別:

==本質(zhì)來講兩者區(qū)別不大祸穷,其源碼實現(xiàn)是一樣的性穿,只不過使用場景不同,context.Background()通常由主函數(shù)雷滚、初始化和測試使用需曾,是頂級Context;context.TODO()通常用于主協(xié)程外的其他協(xié)程向下傳遞,分析工具可識別它在調(diào)用棧中傳播呆万。==

3.3 派生Context

除以上兩種頂級Context類型商源,context包提供四種創(chuàng)建可派生Context類型的函數(shù):

(1). func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel函數(shù)返回具有新done通道的父級副本。當(dāng)調(diào)用返回的cancel函數(shù)或關(guān)閉父上下文的done通道時(以先發(fā)生者為準(zhǔn))谋减,將關(guān)閉返回的上下文的done通道牡彻。
取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用Cancel逃顶。

官方使用示例:

// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
    dst := make(chan int)
    n := 1
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // returning not to leak the goroutine
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
    fmt.Println(n)
    if n == 5 {
        break
    }
}

//OUTPUT:
1
2
3
4
5

(2). func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithDeadline函數(shù)返回父上下文的副本讨便,其截止時間調(diào)整為不遲于d。如果父上下文的截止時間早于d以政,則WithDeadline(Parent霸褒,d)在語義上等同于父上下文。當(dāng)截止時間到期盈蛮、調(diào)用返回的cancel函數(shù)或關(guān)閉父上下文的done通道(以先發(fā)生者為準(zhǔn))時废菱,返回的上下文的done通道將關(guān)閉。
取消此上下文將釋放與其關(guān)聯(lián)的資源抖誉,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用Cancel殊轴。

官方使用示例:
這個例子傳遞一個具有任意截止時間的上下文,告訴一個阻塞函數(shù)一旦到達(dá)它就應(yīng)該放棄它的工作袒炉。

d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

//OUTPUT:
context deadline exceeded
(3). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))旁理。取消此上下文將釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用取消:

官方使用示例:這個例子傳遞一個帶有超時的上下文我磁,告訴一個阻塞函數(shù)它應(yīng)該在超時結(jié)束后放棄它的工作孽文。

// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}


//OUTPUT:
context deadline exceeded

*以上函數(shù)的特殊返回類型值:type CancelFunc func()

CancelFunc告訴操作放棄其工作。CancelFunc不等待工作停止夺艰。在第一次調(diào)用之后芋哭,對CancelFunc的后續(xù)調(diào)用不做任何操作。

(4). func WithValue(parent Context, key, val interface{}) Context

WithValue返回父級的副本郁副,可為上下文設(shè)置一個鍵值對减牺。
只對傳輸進(jìn)程和API的請求范圍數(shù)據(jù)使用上下文值,而不用于向函數(shù)傳遞可選參數(shù)存谎。
提供的鍵必須是可比較的拔疚,并且不應(yīng)是字符串或任何其他內(nèi)置類型龙优,以避免使用上下文的包之間發(fā)生沖突世杀。WithValue的用戶應(yīng)該為鍵定義自己的類型。為了避免在分配給接口時進(jìn)行分配赃梧,上下文鍵通常具有具體的類型結(jié)構(gòu)固以。或者,導(dǎo)出的上下文鍵變量的靜態(tài)類型應(yīng)該是指針或接口憨琳。

官方使用示例:

type favContextKey string

f := func(ctx context.Context, k favContextKey) {
    if v := ctx.Value(k); v != nil {
        fmt.Println("found value:", v)
        return
    }
    fmt.Println("key not found:", k)
}

k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")

f(ctx, k)
f(ctx, favContextKey("color"))


//OUTPUT:
found value: Go
key not found: color

三诫钓、Context使用示例

使用context包來實現(xiàn)線程安全退出或超時的控制:


//定義一個并發(fā)worker
func worker(ctx context.Context, wg *sync.WaitGroup) error {
    defer wg.Done()

    for {
        select {
        //當(dāng)父協(xié)程調(diào)用cancel()時,會從ctx.Done()得到struct{}篙螟,此時返回ctx.Err()退出子線程
        case <-ctx.Done():
            return ctx.Err()
        default:
        //默認(rèn)輸出hello
        fmt.Println("hello")
        }
    }
}

func main() {
    //生成一個有超時控制的衍生Context菌湃,超時10s退出所有子協(xié)程
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(ctx, &wg)
    }
    
    //主協(xié)程1s后就cancel所有子協(xié)程了,每個worker都可以安全退出
    time.Sleep(time.Second)
    cancel()
    wg.Wait()
}

當(dāng)并發(fā)體超時或main主動停止工作者Goroutine時遍略,每個工作者都可以安全退出惧所。

Go語言是帶內(nèi)存自動回收特性的,因此內(nèi)存一般不會泄漏绪杏。當(dāng)main函數(shù)不再使用管道時后臺Goroutine有泄漏的風(fēng)險下愈。我們可以通過context包來避免這個問題,下面是防止內(nèi)存泄露的素數(shù)篩實現(xiàn):


// 返回生成自然數(shù)序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
    ch := make(chan int)
    go func() {
        for i := 2; ; i++ {
            select {
            //父協(xié)程cancel()時安全退出該子協(xié)程
            case <- ctx.Done():
                return
            //生成的素數(shù)發(fā)送到管道
            case ch <- i:
            }
        }
    }()
    return ch
}

// 管道過濾器: 刪除能被素數(shù)整除的數(shù)
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
    out := make(chan int)
    go func() {
        for {
            if i := <-in; i%prime != 0 {
                select {
                //父協(xié)程cancel()時安全退出該子協(xié)程
                case <- ctx.Done():
                    return
                case out <- i:
                }
            }
        }
    }()
    return out
}

func main() {
    // 使用一個可由父協(xié)程控制子協(xié)程安全退出的Context蕾久。
    ctx, cancel := context.WithCancel(context.Background())

    ch := GenerateNatural(ctx) // 自然數(shù)序列: 2, 3, 4, ...
    
    for i := 0; i < 100; i++ {
        // 新出現(xiàn)的素數(shù)打印出來
        prime := <-ch 
        fmt.Printf("%v: %v\n", i+1, prime)
        // 基于新素數(shù)構(gòu)造的過濾器
        ch = PrimeFilter(ctx, ch, prime) 
    }
    
    //輸出100以內(nèi)符合要求的素數(shù)后安全退出所有子協(xié)程
    cancel()
}

當(dāng)main函數(shù)完成工作前势似,通過調(diào)用cancel()來通知后臺Goroutine退出,這樣就避免了Goroutine的泄漏僧著。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末履因,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子盹愚,更是在濱河造成了極大的恐慌栅迄,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皆怕,死亡現(xiàn)場離奇詭異毅舆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)端逼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門朗兵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人顶滩,你說我怎么就攤上這事余掖。” “怎么了礁鲁?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵盐欺,是天一觀的道長。 經(jīng)常有香客問我仅醇,道長冗美,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任析二,我火速辦了婚禮粉洼,結(jié)果婚禮上节预,老公的妹妹穿的比我還像新娘。我一直安慰自己属韧,他們只是感情好安拟,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宵喂,像睡著了一般糠赦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锅棕,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天拙泽,我揣著相機(jī)與錄音,去河邊找鬼裸燎。 笑死顾瞻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的顺少。 我是一名探鬼主播朋其,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼脆炎!你這毒婦竟也來了梅猿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤秒裕,失蹤者是張志新(化名)和其女友劉穎袱蚓,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體几蜻,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喇潘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了梭稚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颖低。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖弧烤,靈堂內(nèi)的尸體忽然破棺而出忱屑,到底是詐尸還是另有隱情,我是刑警寧澤暇昂,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布莺戒,位于F島的核電站,受9級特大地震影響急波,放射性物質(zhì)發(fā)生泄漏从铲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一澄暮、第九天 我趴在偏房一處隱蔽的房頂上張望名段。 院中可真熱鬧阱扬,春花似錦、人聲如沸伸辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽自娩。三九已至,卻和暖如春渠退,著一層夾襖步出監(jiān)牢的瞬間忙迁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工碎乃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姊扔,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓梅誓,卻偏偏與公主長得像恰梢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梗掰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容