詳解Go語(yǔ)言中上下文context的理解與使用
為什么需要 context
在 Go 程序中,特別是并發(fā)情況下,由于超時(shí)、取消等而引發(fā)的異常操作,往往需要及時(shí)的釋放相應(yīng)資源,正確的關(guān)閉 goroutine。防止協(xié)程不退出而導(dǎo)致內(nèi)存泄露。如果沒(méi)有 context,用來(lái)控制協(xié)程退出將會(huì)非常麻煩,我們來(lái)舉一個(gè)例子。
假如說(shuō)現(xiàn)在一個(gè)協(xié)程A開(kāi)啟了一個(gè)子協(xié)程B,這個(gè)子協(xié)程B又開(kāi)啟了另外兩個(gè)子協(xié)程B1和B2來(lái)運(yùn)行不同的任務(wù),協(xié)程B2又開(kāi)啟了協(xié)程C來(lái)運(yùn)行其他任務(wù),現(xiàn)在協(xié)程A通知子協(xié)程B該退出了,這個(gè)時(shí)候我們需要完成這樣的操作:A通知B退出,B退出時(shí)通知B1、B2退出,B2退出時(shí)通知C退出:
func TestChanCloseGoroutine(t *testing.T) { fmt.Printf("開(kāi)始了,有%d個(gè)協(xié)程\n", runtime.NumGoroutine()) var ( chB = make(chan struct{}) chB1 = make(chan struct{}) chB2 = make(chan struct{}) chC = make(chan struct{}) ) // 協(xié)程A go func() { // 協(xié)程B go func() { // 協(xié)程B1 go func() { for { select { case <-chB1: return default: } } }() // 協(xié)程B2 go func() { // 協(xié)程C go func() { for { select { case <-chC: return default: } } }() for { select { case <-chB2: // 通知協(xié)程C退出 chC <- struct{}{} return default: } } }() for { select { case <-chB: chB1 <- struct{}{} chB2 <- struct{}{} return default: } } }() // 1秒后通知B退出 time.Sleep(1 * time.Second) chB <- struct{}{} // A后續(xù)沒(méi)有任務(wù)了,會(huì)自動(dòng)退出 }() time.Sleep(2 * time.Second) fmt.Printf("最終結(jié)束,有%d個(gè)協(xié)程\n", runtime.NumGoroutine()) } // 結(jié)果 開(kāi)始了,有2個(gè)協(xié)程 最終結(jié)束,有2個(gè)協(xié)程 // tips: Go Test 會(huì)啟動(dòng)兩個(gè)額外的 goroutine 來(lái)運(yùn)行代碼,所以初始就會(huì)有2個(gè) goroutine
通過(guò) channel 來(lái)控制各個(gè) goroutine 的關(guān)閉,程序看上去一點(diǎn)也不優(yōu)雅。而且這才僅僅四個(gè) goroutine ,就已經(jīng)顯得有些力不從心了,在真實(shí)的業(yè)務(wù)中,哪怕一個(gè)簡(jiǎn)單的 http 請(qǐng)求,都不可能啟用四個(gè) goroutine 就能夠完成,且子協(xié)程的層級(jí)也絕非只有寥寥的三層!
context 是什么
context 在 Go 中是一個(gè)接口,它的定義如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
- Deadline 用來(lái)獲取 ctx 的截止時(shí)間,如果沒(méi)有截至?xí)r間,ok 將返回 false;
- Done 里面是一個(gè)通道,當(dāng) ctx 被取消時(shí),會(huì)返回一個(gè)關(guān)閉的 channel,如果該 ctx 永遠(yuǎn)都不會(huì)被關(guān)閉,則返回 nil;
- Err 返回的 ctx 取消的原因,如果 ctx 沒(méi)有被取消,會(huì)返回 nil。如果已經(jīng)關(guān)閉了,會(huì)返回被關(guān)閉的原因,如果是被取消的會(huì)返回 canceled,超時(shí)的顯示 deadline exceeded;
- Value 會(huì)返回 ctx 中儲(chǔ)存的值,會(huì)從當(dāng)前 ctx 中一路向上追溯,如果整條 ctx 鏈中都沒(méi)有找到值,則會(huì)返回nil。
context 的基本結(jié)構(gòu)比較簡(jiǎn)單,里面也只有四個(gè)方法,如果到此沒(méi)有理解四個(gè)方法也沒(méi)有關(guān)系,下文會(huì)使用到這四個(gè)方法,屆時(shí)將會(huì)很自然的掌握它們。
context 接口的實(shí)現(xiàn)
context 有四個(gè)不同的實(shí)現(xiàn):emptyCtx、cancelCtx、timerCtx、valueCtx:
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 any) any { return nil }
emptyCtx 是一個(gè)實(shí)現(xiàn)了 context 接口的整型,它不能儲(chǔ)存信息,也不能被取消,它被當(dāng)作根節(jié)點(diǎn) ctx。cancelCtx、timerCtx、valueCtx 由于篇幅原因,這里不放出它們的源碼,只解釋它們的作用:cancelCtx 是一個(gè)可以主動(dòng)取消的 ctx。timerCtx 也是一個(gè)可以主動(dòng)取消的 ctx,不同于 cancelCtx,它還儲(chǔ)存著額外的時(shí)間信息,當(dāng)時(shí)間條件滿足后,會(huì)自動(dòng)取消該 ctx,利用這點(diǎn),可以實(shí)現(xiàn)超時(shí)機(jī)制。valueCtx 比較簡(jiǎn)單,用來(lái)創(chuàng)建一個(gè)攜帶鍵值的 ctx。
context 的基本使用
創(chuàng)建一個(gè)根節(jié)點(diǎn)
創(chuàng)建根節(jié)點(diǎn)有兩種方法:
ctx := context.Background() ctx := context.TODO()
這兩種方法其實(shí)本質(zhì)上都是初始化了一個(gè) emptyCtx:
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
可以看到,在代碼中,這兩個(gè)函數(shù)其實(shí)是一模一樣的,只是用于不同場(chǎng)景下:Background 推薦在主函數(shù)、初始化和測(cè)試中使用,TODO 用于不清楚使用哪個(gè) context 時(shí)使用。根節(jié)點(diǎn) ctx 不具備任何意義,也不能被取消。
創(chuàng)建一個(gè)子 ctx
可以通過(guò)WithCancel、WithDeadline、WithTimeout、WithValue 這四個(gè)主要的函數(shù)來(lái)創(chuàng)建子 ctx ,創(chuàng)建一個(gè)子 ctx 必須指定其歸屬的父 ctx,由此來(lái)形成一個(gè)上下文鏈,用來(lái)同步 goroutine 信號(hào)。來(lái)看一下它們的簡(jiǎn)單使用:
WithCancel
用來(lái)創(chuàng)建一個(gè) cancelCtx,它可以被主動(dòng)取消 :
func TestCtxWithCancel(t *testing.T) { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) go func() { for { select { // 還記得前文提到的Done的方法嗎 // 當(dāng) ctx 取消時(shí),ctx.Done()對(duì)應(yīng)的通道就會(huì)關(guān)閉,case也就會(huì)被執(zhí)行 case <-ctx.Done(): // ctx.Err() 會(huì)獲取到關(guān)閉原因哦 fmt.Println("協(xié)程關(guān)閉", ctx.Err()) return default: fmt.Println("繼續(xù)運(yùn)行") time.Sleep(100 * time.Millisecond) } } }() // 等待一秒后關(guān)閉 time.Sleep(1 * time.Second) cancel() // 等待一秒,讓子協(xié)程有時(shí)間打印出協(xié)程關(guān)閉的原因 time.Sleep(1 * time.Second) } // 結(jié)果 繼續(xù)運(yùn)行 繼續(xù)運(yùn)行 …… 協(xié)程關(guān)閉 context canceled
WithDeadline
用來(lái)創(chuàng)建一個(gè) timerCtx,當(dāng)時(shí)間條件滿足后,它會(huì)被自動(dòng)取消 :
func TestCtxWithDeadline(t *testing.T) { ctx := context.Background() // 等待2秒后自動(dòng)關(guān)閉 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second)) defer cancel() // Deadline 前文也提到了,還記得嗎?用來(lái)獲取當(dāng)前任務(wù)的截至?xí)r間 if t, ok := ctx.Deadline(); ok { // time.DateTime 是 go1.20 版本的一個(gè)常量,其值是:"2006-01-02 15:04:05" fmt.Println(t.Format(time.DateTime)) } go func() { select { case <-ctx.Done(): // 手動(dòng)關(guān)閉 context canceled // 自動(dòng)關(guān)閉 context deadline exceeded fmt.Println("協(xié)程關(guān)閉", ctx.Err()) return } }() time.Sleep(3 * time.Second) } // 結(jié)果 2023-05-10 18:00:36 協(xié)程關(guān)閉 context deadline exceeded // 將最后的等待時(shí)間更改為一秒 func TestCtxWithDeadline(t *testing.T) { …… time.Sleep(1 * time.Second) } // 結(jié)果 2023-05-10 18:01:45 協(xié)程關(guān)閉 context canceled
哪怕 WithDeadline 到達(dá)指定時(shí)間會(huì)自動(dòng)關(guān)閉,但依然推薦使用 defer cancel() 。這是因?yàn)槿绻蝿?wù)已經(jīng)完成了,但是自動(dòng)取消仍需要1天時(shí)間,那么系統(tǒng)就會(huì)白白浪費(fèi)資源在這1天上。
WithTimeout
與 WithDeadline
同理,只不過(guò)是 WithTimeout 用來(lái)接受一個(gè)過(guò)期時(shí)間,而不是接受一個(gè)過(guò)期時(shí)間節(jié)點(diǎn):
func TestCtxWithTimeout(t *testing.T) { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() go func() { select { case <-ctx.Done(): fmt.Println("協(xié)程關(guān)閉", ctx.Err()) return } }() time.Sleep(3 * time.Second) } // 結(jié)果 協(xié)程關(guān)閉 context deadline exceeded
WithValue
用來(lái)創(chuàng)建一個(gè) valueCtx:
// 向上找到最近的上下文值 func TestCtxWithValue(t *testing.T) { ctx := context.Background() ctx1 := context.WithValue(ctx, "key", "ok") ctx2, _ := context.WithCancel(ctx1) // Value 會(huì)一直向上追溯到根節(jié)點(diǎn),獲取當(dāng)前上下文攜帶的值, value := ctx2.Value("key") if value != nil { fmt.Println(value) } } // 結(jié)果 ok
這四個(gè)函數(shù)都是創(chuàng)建一個(gè)新的子節(jié)點(diǎn),并不是直接修改當(dāng)前 ctx,所以最后生成的 ctx 鏈有可能是這樣的:
使用 ctx 退出 goroutine
回到開(kāi)頭提到的那個(gè)例子,我們使用 context 對(duì)其改造一下:
func TestCtxCloseGoroutine(t *testing.T) { fmt.Printf("開(kāi)始了,有%d個(gè)協(xié)程\n", runtime.NumGoroutine()) ctx := context.Background() // 協(xié)程A go func(ctx context.Context) { ctx, cancel := context.WithCancel(ctx) // 協(xié)程B go func(ctx context.Context) { // 協(xié)程B1 go func(ctx context.Context) { for { select { case <-ctx.Done(): return default: } } }(ctx) // 協(xié)程B2 go func(ctx context.Context) { // 協(xié)程C go func(ctx context.Context) { for { select { case <-ctx.Done(): return default: } } }(ctx) for { select { case <-ctx.Done(): return default: } } }(ctx) for { select { case <-ctx.Done(): return default: } } }(ctx) // 1秒后通知退出 time.Sleep(1 * time.Second) cancel() // A后續(xù)沒(méi)有任務(wù)了,會(huì)自動(dòng)退出 }(ctx) time.Sleep(2 * time.Second) fmt.Printf("最終結(jié)束,有%d個(gè)協(xié)程\n", runtime.NumGoroutine()) } // 結(jié)果 開(kāi)始了,有2個(gè)協(xié)程 最終結(jié)束,有2個(gè)協(xié)程
可以看到,和使用 channel 控制 goroutine 退出相比,context 大大降低了心智負(fù)擔(dān)。context 優(yōu)雅的實(shí)現(xiàn)了某一層任務(wù)退出,下層所有任務(wù)退出,上層任務(wù)和同層任務(wù)不受影響。
Go 語(yǔ)言最佳實(shí)踐:每次 context 的傳遞都應(yīng)該直接使用值傳遞,不應(yīng)該使用指針傳遞。這樣可以防止上下文的值被多個(gè)并發(fā)的 goroutine 修改而導(dǎo)致競(jìng)爭(zhēng)問(wèn)題。雖然使用值傳遞會(huì)導(dǎo)致一些微小的性能開(kāi)銷(xiāo),因?yàn)槊看蝹鬟f上下文時(shí)都需要復(fù)制一份數(shù)據(jù),但它提供了更好的并發(fā)安全性和程序可靠性。另外,由于上下文采用了值傳遞,也不應(yīng)該向上下文中存入較大的數(shù)據(jù),從而導(dǎo)致性能問(wèn)題。
以上就是詳解Go語(yǔ)言中上下文context的理解與使用的詳細(xì)內(nèi)容,更多關(guān)于go上下文context的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang中的string與其他格式數(shù)據(jù)的轉(zhuǎn)換方法詳解
這篇文章主要介紹了golang中的string與其他格式數(shù)據(jù)的轉(zhuǎn)換方法,文章通過(guò)代碼示例介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)操作
這篇文章在 go-kratos 官方的 layout 項(xiàng)目的整潔架構(gòu)基礎(chǔ)上,實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)庫(kù)事務(wù)操作,需要的朋友可以參考下2024-08-08sublime3+Golang+代碼補(bǔ)全的實(shí)現(xiàn)
本文主要介紹了sublime3+Golang+代碼補(bǔ)全的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01golang?基于?mysql?簡(jiǎn)單實(shí)現(xiàn)分布式讀寫(xiě)鎖
這篇文章主要介紹了golang?基于mysql簡(jiǎn)單實(shí)現(xiàn)分布式讀寫(xiě)鎖,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09Go語(yǔ)言之使用pprof工具查找goroutine(協(xié)程)泄漏
這篇文章主要介紹了Go語(yǔ)言之使用pprof工具查找goroutine(協(xié)程)泄漏,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01