Context 通常被譯作上下文乾胶,一般理解為程序單元的一個運行狀態(tài)封豪、現(xiàn)場谴轮、快照,而翻譯中上下文又很好地詮釋了它的本質(zhì)吹埠,上下則是指存在上下層的傳遞第步,上會把內(nèi)容傳遞給下。
在 Golang 中缘琅,程序單元也就是指的 goroutine粘都。每個 goroutine 在執(zhí)行之前,都要先知道程序當(dāng)前的執(zhí)行狀態(tài)刷袍,通常將這些執(zhí)行狀態(tài)封裝在一個 Context 變量中翩隧,傳遞給要執(zhí)行的 Goroutine 中。上下文則幾乎已經(jīng)成為傳遞與請求同生存周期變量的標(biāo)準(zhǔn)方法呻纹。
context 包不僅實現(xiàn)了在程序單元之間共享狀態(tài)變量的方法堆生,同時能通過簡單的方法,使我們在被調(diào)用程序單元的外部雷酪,通過設(shè)置 ctx 變量值淑仆,將過期或撤銷這些信號傳遞給被調(diào)用的單元。在網(wǎng)絡(luò)編程中太闺,若存在A調(diào)用B的API糯景,B再調(diào)用C的API,若A調(diào)用B取消省骂,則也要取消B調(diào)用C蟀淮,通過在A、B钞澳、C的API調(diào)用之間傳遞Context怠惶,以及判斷其狀態(tài),就能解決此問題轧粟,這是為什么 gRPC 的接口中帶上 ctx context.Context 參數(shù)的原因之一策治。
context 包的核心就是 Context 接口脓魏,其定義如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline 會返回一個超時時間,Goroutine 獲得了超時時間后通惫,例如可以對某些 io 操作設(shè)定超時時間茂翔。
- Done 方法返回一個信道(channel),當(dāng) Context 被撤銷或過期時履腋,該信道是關(guān)閉的珊燎,即它是一個表示 Context 是否關(guān)閉的信號。
- 當(dāng) Done 信道關(guān)閉后遵湖,Err方法表明 Context 被撤銷的原因悔政。
- Value 可以讓 Goroutine 共享一些數(shù)據(jù),當(dāng)然獲得數(shù)據(jù)是協(xié)程安全的延旧。但是使用這些數(shù)據(jù)的時候谋国,需要注意同步,比如返回了一個 map迁沫,而這個 map 的讀寫則要加鎖芦瘾。
Context 接口沒有提供方法來設(shè)置其值和過期時間,也沒有提供方法直接將其自身撤銷弯洗。也就是說旅急,Context 不能改變和撤銷自身逢勾。那么該怎么通過 Context 傳遞改變后的狀態(tài)呢牡整?
context 使用
無論是 Goroutine,他們的創(chuàng)建和調(diào)用關(guān)系總是像層層調(diào)用進(jìn)行的溺拱,就像人的輩分一樣逃贝,而更靠頂部的 Goroutine 應(yīng)有辦法主動關(guān)閉其下屬的 Goroutine 的執(zhí)行(不然程序就可能失控了)。為了實現(xiàn)這種關(guān)系迫摔,Context 結(jié)構(gòu)也應(yīng)該是樹狀沐扳,葉子節(jié)點總由根節(jié)點衍生出來。
要創(chuàng)建 Context 樹句占,第一步就是要得到根節(jié)點沪摄, context.Background 函數(shù)的返回值就是根節(jié)點:
func Background() Context
該函數(shù)返回空的 Context,該 Context 一般由接收請求的第一個 Goroutine 創(chuàng)建纱烘,是與進(jìn)入請求對應(yīng)的 Context 根節(jié)點杨拐,它不能取消、沒有值擂啥、也沒有過期時間哄陶。它常常作為處理 Request 的頂層 context 存在。
有了根節(jié)點哺壶,又該怎么創(chuàng)建其他的子節(jié)點屋吨、孫節(jié)點蜒谤? context 包為我們提供了多個函數(shù)來創(chuàng)建他們:
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 interface{}, val interface{}) Context
函數(shù)接收一個 Context 類型的參數(shù) parent,并返回一個 Context 類型的值至扰,這樣就層層創(chuàng)建出不同的節(jié)點鳍徽。子節(jié)點是從復(fù)制父節(jié)點得到的,并且根據(jù)接收參數(shù)設(shè)定子節(jié)點的一些狀態(tài)值敢课,接著就可以將子節(jié)點傳遞給下層的 Goroutine 了旬盯。
再回到之前的問題:該怎么通過 Context 傳遞改變后的狀態(tài)呢?使用 Context 的 Goroutine 無法取消某個操作翎猛,其實是合理的胖翰,因為這些 Goroutine 是被某個父 Goroutine 創(chuàng)建的,而理應(yīng)只有父 Goroutine 可以取消操作切厘。在父 Goroutine 中可以通過 WithCancel 方法獲得一個 cancel 方法萨咳,從而獲得 cancel 的權(quán)利。
第一個 WithCancel 函數(shù)疫稿,它是將父節(jié)點復(fù)制到子節(jié)點培他,并且還返回一個額外的 CancelFunc 函數(shù)類型變量,該函數(shù)類型的定義為:
type CancelFunc func()
調(diào)用 CancelFunc 對象將撤銷對應(yīng)的 Context 對象遗座,這就是主動撤銷 Context 的方法舀凛。在父節(jié)點的 Context 所對應(yīng)的環(huán)境中,通過 WithCancel 函數(shù)不僅可創(chuàng)建子節(jié)點的 Context途蒋,同時也獲得了該節(jié)點 Context 的控制權(quán)猛遍,一旦執(zhí)行該函數(shù),則該節(jié)點 Context 就結(jié)束了号坡,則子節(jié)點需要類似如下代碼來判斷是否已結(jié)束懊烤,并退出該 Goroutine:
select {
case <-ctx.Done():
// do some clean ...
}
WithDeadline 函數(shù)的作用也差不多,它返回 Context 的類型同樣是 parent 的副本宽堆,但其過期時間由 deadline 和 parent 的過期時間同時決定腌紧。當(dāng) parent 的過期時間早于傳入的 deadline 時間時,返回的過期時間應(yīng)與 parent 相同畜隶。父節(jié)點過期時壁肋,其所有的子孫節(jié)點必須同時關(guān)閉;反之籽慢,返回的父節(jié)點的過期時間則為 deadline浸遗。
WithTimeout 函數(shù)與 WithDeadline 類似,只不過它傳入的是從現(xiàn)在開始 Context 剩余的生命時長嗡综。他們都同樣返回了所創(chuàng)建的子 Context 的控制權(quán)乙帮,一個 CancelFunc 類型的函數(shù)變量。
當(dāng)頂層的 Request 請求函數(shù)結(jié)束后极景,我們就可以 cancel 掉某個 context察净,從而層層 Goroutine 根據(jù)判斷 ctx.Done() 來結(jié)束驾茴。
WithValue 函數(shù),它返回 parent 的一個函數(shù)副本氢卡,調(diào)用該副本的 Value(key) 方法將得到 val锈至。這樣,我們不光將根節(jié)點的原有的值保留了译秦,還在孫節(jié)點中加入了新的值峡捡,注意若存在Key相同,則會被覆蓋筑悴。