詳解Golang中Context的原理和使用技巧
Context 背景 和 適用場(chǎng)景
Context 的背景
Golang 在 1.6.2 的時(shí)候還沒(méi)有自己的 context,在1.7的版本中就把 https://pkg.go.dev/golang.org/x/net/context包被加入到了官方的庫(kù)中。Golang 的 Context 包,中文可以稱之為“上下文”,是用來(lái)在 goroutine 協(xié)程之間進(jìn)行上下文信息傳遞的,這些上下文信息包括 kv 數(shù)據(jù)、取消信號(hào)、超時(shí)時(shí)間、截止時(shí)間等。
Context 的功能和目的
雖然我們知道了 context 上下文的基本信息,但是想想,為何 Go 里面把 Context 單獨(dú)擰出來(lái)設(shè)計(jì)呢?這就和 Go 的并發(fā)有比較大的關(guān)系,因?yàn)?Go 里面創(chuàng)建并發(fā)協(xié)程非常容易,但是,如果沒(méi)有相關(guān)的機(jī)制去控制這些這些協(xié)程的生命周期,那么可能導(dǎo)致協(xié)程泛濫,也可能導(dǎo)致請(qǐng)求大量超時(shí),協(xié)程無(wú)法退出導(dǎo)致協(xié)程泄漏、協(xié)程泄漏導(dǎo)致協(xié)程占用的資源無(wú)法釋放,從而導(dǎo)致資源被占滿等各種問(wèn)題。所以,context 出現(xiàn)的目的就是為了解決并發(fā)協(xié)程之間父子進(jìn)程的退出控制。
一個(gè)常見(jiàn)例子,有一個(gè) web 服務(wù)器,來(lái)一個(gè)請(qǐng)求,開(kāi)多個(gè)協(xié)程去處理這個(gè)請(qǐng)求的業(yè)務(wù)邏輯,比如,查詢登錄狀態(tài)、獲取用戶信息、獲取業(yè)務(wù)信息等,那么如果請(qǐng)求的下游協(xié)程的生命周期無(wú)法控制,那么我們的業(yè)務(wù)請(qǐng)求就可能會(huì)一直超時(shí),業(yè)務(wù)服務(wù)可能會(huì)因?yàn)閰f(xié)程沒(méi)有釋放導(dǎo)致協(xié)程泄漏。因此,協(xié)程之間能夠進(jìn)行事件通知并且能控制協(xié)程的生命周期非常重要,怎么實(shí)現(xiàn)呢? context 就是來(lái)干這些事的。
另外,既然有大量并發(fā)協(xié)程,那么各個(gè)協(xié)程之間的一些基礎(chǔ)數(shù)據(jù)如果想要共享,比如把每個(gè)請(qǐng)求鏈路的 tarceID 都進(jìn)行傳遞,這樣把整個(gè)鏈路串起來(lái),要怎么做呢? 還是要依靠 context。
總體來(lái)說(shuō),context 的目的主要包括兩個(gè):
- 協(xié)程之間的事件通知(超時(shí)、取消)
- 協(xié)程之間的數(shù)據(jù)傳遞鍵值對(duì)的數(shù)據(jù)(kv 數(shù)據(jù))
Context 的基本使用
Go 語(yǔ)言中的 Context 直接使用官方的 "context"
包就可以開(kāi)始使用了,一般是在我們所有要傳遞的地方(函數(shù)的第一個(gè)參數(shù))把 context.Context 類型的變量傳遞,并對(duì)其進(jìn)行相關(guān) API 的使用。context 常用的使用姿勢(shì)包括但不限于:
- 通過(guò) context 進(jìn)行數(shù)據(jù)傳遞,但是這里只能傳遞一些通用或者基礎(chǔ)的元數(shù)據(jù),不要傳遞業(yè)務(wù)層面的數(shù)據(jù),不是說(shuō)不可以傳遞,是在 Go 的編碼規(guī)范或者慣用法中不提倡
- 通過(guò) context 進(jìn)行協(xié)程的超時(shí)控制
- 通過(guò) context 進(jìn)行并發(fā)控制
Context 的同步控制設(shè)計(jì)
Go 里面控制并發(fā)有兩種經(jīng)典的方式,一種是 WaitGroup,另外一種就是 Context。
在 Go 里面,當(dāng)需要進(jìn)行多批次的計(jì)算任務(wù)同步,或者需要一對(duì)多的協(xié)作流程的時(shí)候;通過(guò) Context 的關(guān)聯(lián)關(guān)系(go 的 context 被設(shè)計(jì)為包含了父子關(guān)系),我們就可以控制子協(xié)程的生命周期,而其他的同步方式是無(wú)法控制其生命周期的,只能是被動(dòng)阻塞等待完成或者結(jié)束。context 控制子協(xié)程的生命周期,是通過(guò) context 的 context.WithTimeout 機(jī)制來(lái)實(shí)現(xiàn)的,這個(gè)是一般系統(tǒng)中或者底層各種框架、庫(kù)的普適用法。context 對(duì)并發(fā)做一些控制包括 Context Done 取消、截止時(shí)間取消 context.WithDeadline、超時(shí)取消 context.WithTimeout 等。
比如有一個(gè)網(wǎng)絡(luò)請(qǐng)求 Request,每個(gè) Request 都需要開(kāi)啟一個(gè) goroutine 做一些業(yè)務(wù)邏輯,這些 goroutine 又可能會(huì)開(kāi)啟其他的 goroutine。那么這樣的話,我們就可以通過(guò) Context 來(lái)跟蹤并控制這些 goroutine。
另外一個(gè)實(shí)際例子是,在 Go 實(shí)現(xiàn)的 web server 中,每個(gè)請(qǐng)求都會(huì)開(kāi)一個(gè) goroutine 去處理。但是我們的這個(gè) goroutine 請(qǐng)求邏輯里面, 還需繼續(xù)創(chuàng)建goroutine 去訪問(wèn)后端其他資源,比如數(shù)據(jù)庫(kù)、RPC 服務(wù)等。由于這些 goroutine 都是在處理同一個(gè)請(qǐng)求,因此,如果請(qǐng)求超時(shí)或者被取消后,所有的 goroutine 都應(yīng)該馬上退出并且釋放相關(guān)的資源,這種情況也需要用 Context 來(lái)為我們?nèi)∠羲?goroutine。
Context 的定義和實(shí)現(xiàn)
Context interface 接口定義
在 golang 里面,interface 是一個(gè)使用非常廣泛的結(jié)構(gòu),它可以接納任何類型。而 context 就是通過(guò) interface 來(lái)定義的,定義很簡(jiǎn)單,一共4個(gè)方法,這也是 Go 的設(shè)計(jì)理念,接口盡量簡(jiǎn)單、小巧,通過(guò)組合來(lái)實(shí)現(xiàn)豐富的功能。
定義如下:
type Context interface { // 返回 context 是否會(huì)被取消以及自動(dòng)取消的截止時(shí)間(即 deadline) Deadline() (deadline time.Time, ok bool) // 當(dāng) context 被取消或者到了 deadline,返回一個(gè)被關(guān)閉的 channel Done() <-chan struct{} // 返回取消的錯(cuò)誤原因,因?yàn)槭裁?Context 被取消 Err() error // 獲取 key 對(duì)應(yīng)的 value Value(key interface{}) interface{} }
- Deadline 返回 context 是否會(huì)被取消以及自動(dòng)取消的截止時(shí)間,第一個(gè)返回值是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context 會(huì)自動(dòng)發(fā)起取消請(qǐng)求;第二個(gè)返回值 ok==false 時(shí)表示沒(méi)有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。
- Done 方法返回一個(gè)只讀的 chan,類型為 struct{},如果該方法返回的 chan 可以讀取,那么就說(shuō)明 parent context 已經(jīng)發(fā)起了取消請(qǐng)求,當(dāng)我們通過(guò) Done 方法收到這個(gè)信號(hào)后,就應(yīng)該做清理操作,然后退出 goroutine,釋放資源。之后,Err 方法會(huì)返回一個(gè)錯(cuò)誤,告知為什么 Context 被取消。
- Err 方法返回取消的錯(cuò)誤原因,因?yàn)槭裁?Context 被取消。
- Value 方法獲取該 Context 上保存的鍵值對(duì),所以要通過(guò)一個(gè) Key 才可以獲取對(duì)應(yīng)的值,這個(gè)值一般是線程安全(并發(fā)安全)的。雖然 context 是一個(gè)并發(fā)安全的類型,但是如果 context 中保存著 value,則這些 value 通常不是并發(fā)安全的,并發(fā)讀寫這些 value 可能會(huì)造成數(shù)據(jù)錯(cuò)亂,嚴(yán)重的情況下可能發(fā)生 panic,所以在并發(fā)時(shí),如果我們的業(yè)務(wù)代碼需要讀寫 context 中的 value,那么最好建議我們 clone 一份原來(lái)的 context 中的 value,并塞到新的 ctx 傳遞給各個(gè)gorouinte。當(dāng)然, 如果已經(jīng)明確不會(huì)有并發(fā)讀取,那么可以直接使用,或者使用的時(shí)候加鎖。
parent Context 的具體實(shí)現(xiàn)
Context 雖然是個(gè)接口,但是并不需要使用方實(shí)現(xiàn),golang 內(nèi)置的 context 包,已經(jīng)幫我們實(shí)現(xiàn)了,查看 Go 的源碼可以看到如下定義:
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
Background 和 TODO 兩個(gè)其實(shí)都是基于 emptyCtx 來(lái)實(shí)現(xiàn)的,emptyCtx 類型實(shí)現(xiàn)了 context 接口定義的 4 個(gè)方法,它本身是一個(gè)不可取消,沒(méi)有設(shè)置截止時(shí)間,沒(méi)有攜帶任何值的 Context,查看官方源碼如下:
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 }
Background 方法,一般是在 main 函數(shù)的入口處(或者請(qǐng)求最初的根 context)就定義并使用,然后一直往下傳遞,接下來(lái)所有的子協(xié)程里面都是基于 main 的 context 來(lái)衍生的。TODO 這個(gè)一般不建議業(yè)務(wù)上使用,一般沒(méi)有實(shí)際意義,在單元測(cè)試?yán)锩婵梢允褂谩?/p>
Context 的繼承和各種 With 系列函數(shù)
查看官方文檔 https://pkg.go.dev/golang.org/x/net/context
// 最基礎(chǔ)的實(shí)現(xiàn),也可以叫做父 context func Background() Context func TODO() Context // 在 Background() 根 context 基礎(chǔ)上派生的各種 With 系列函數(shù) 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
- WithCancel 函數(shù),傳遞一個(gè) parent Context 作為參數(shù),返回子 Context,以及一個(gè)取消函數(shù)用來(lái)取消 Context。我們前面說(shuō)到控制父子協(xié)程的生命周期,那么就可以通過(guò)這個(gè)函數(shù)來(lái)實(shí)現(xiàn)
- WithDeadline 函數(shù),和 WithCancel 差不多,但是它會(huì)多傳遞一個(gè)截止時(shí)間參數(shù),這樣的話,當(dāng)?shù)搅私刂沟臅r(shí)間點(diǎn),就會(huì)自動(dòng)取消 Context,當(dāng)然我們也可以不等到這個(gè)時(shí)候,然后可以通過(guò)取消函數(shù)提前進(jìn)行取消。
- WithTimeout 函數(shù),和 WithDeadline 基本上一樣,會(huì)傳入一個(gè) timeout 超時(shí)時(shí)間,也就是是從現(xiàn)在開(kāi)始,直到過(guò)來(lái) timeout 時(shí)間后,就進(jìn)行超時(shí)取消,注意,這個(gè)是超時(shí)取消,不是截止時(shí)間取消。
- WithValue 函數(shù),這個(gè)和 WithCancel 就沒(méi)有關(guān)系了,它不是用來(lái)控制父子協(xié)程生命周期的,這個(gè)是我們說(shuō)到的,在 context 中傳遞基礎(chǔ)元數(shù)據(jù)用的,這個(gè)可以在 context 中存儲(chǔ)鍵值對(duì)的數(shù)據(jù),然后這個(gè)鍵值對(duì)的數(shù)據(jù)可以通過(guò) Context.Value 方法獲取到,這是我們實(shí)際用經(jīng)常要用到的技巧,一般我們想要通過(guò)上下文來(lái)傳遞數(shù)據(jù)時(shí),可以通過(guò)這個(gè)方法,如我們需要 tarceID 追蹤系統(tǒng)調(diào)用棧的時(shí)候。
Context 的常用方法實(shí)例
1. 調(diào)用 Context Done方法取消
func ContextDone(ctx context.Context, out chan<- Value) error { for { v, err := AllenHandler(ctx) if err != nil { return err } select { case <-ctx.Done(): log.Infof("context has done") return ctx.Err() case out <- v: } } }
2. 通過(guò) context.WithValue 來(lái)傳值
func main() { ctx, cancel := context.WithCancel(context.Background()) valueCtx := context.WithValue(ctx, key, "add value from allen") go watchAndGetValue(valueCtx) time.Sleep(10 * time.Second) cancel() time.Sleep(5 * time.Second) } func watchAndGetValue(ctx context.Context) { for { select { case <-ctx.Done(): //get value log.Infof(ctx.Value(key), "is cancel") return default: //get value log.Infof(ctx.Value(key), "int goroutine") time.Sleep(2 * time.Second) } } }
3. 超時(shí)取消 context.WithTimeout
package main import ( "fmt" "sync" "time" "golang.org/x/net/context" ) var ( wg sync.WaitGroup ) func work(ctx context.Context) error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) // we received the signal of cancelation in this channel case <-ctx.Done(): fmt.Println("Cancel the context ", i) return ctx.Err() } } return nil } func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished. I'm going home") }
4. 截止時(shí)間取消 context.WithDeadline
package main import ( "context" "fmt" "time" ) func main() { d := time.Now().Add(1 * time.Second) 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(2 * time.Second): fmt.Println("oversleep") case <-ctx.Done(): fmt.Println(ctx.Err()) } }
Context 使用原則 和 技巧
- Context 是線程安全的,可以放心的在多個(gè) goroutine 協(xié)程中傳遞
- 可以把一個(gè) Context 對(duì)象傳遞給任意個(gè)數(shù)的 gorotuine,對(duì)它執(zhí)行 取消 操作時(shí),所有 goroutine 都會(huì)接收到取消信號(hào)。
- 不要把 Context 放在結(jié)構(gòu)體中,要以參數(shù)的方式傳遞,parent Context 一般為Background,并且一般要在 main 函數(shù)的入口處創(chuàng)建然后傳遞下去
- Context 的變量名建議都統(tǒng)一為 ctx,并且要把 Context 作為第一個(gè)參數(shù)傳遞給入口請(qǐng)求和出口請(qǐng)求鏈路上的每一個(gè)函數(shù)
- 往下游給一個(gè)函數(shù)方法傳遞 Context 的時(shí)候,千萬(wàn)不要傳遞 nil,否則在 tarce 追蹤的時(shí)候,就會(huì)中斷鏈路,并且如果函數(shù)里面有獲取值的邏輯,可能導(dǎo)致 panic。
- Context 的 Value 只能傳遞一些通用或者基礎(chǔ)的元數(shù)據(jù),不要傳遞業(yè)務(wù)層面的數(shù)據(jù),不是說(shuō)不可以傳遞,是在 Go 的編碼規(guī)范或者慣用法中不提倡不要什么數(shù)據(jù)都使用這個(gè)傳遞。由于 context 存儲(chǔ) key-value 是鏈?zhǔn)降?,因此查詢?fù)雜度為O(n),所以,盡量不要隨意存儲(chǔ)不必要的數(shù)據(jù)
到此這篇關(guān)于詳解Golang中Context的原理和使用技巧的文章就介紹到這了,更多相關(guān)Golang Context內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言開(kāi)發(fā)必知的一個(gè)內(nèi)存模型細(xì)節(jié)
這篇文章主要為大家介紹了Go語(yǔ)言開(kāi)發(fā)必知的一個(gè)內(nèi)存模型細(xì)節(jié)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Qt6.5 grpc組件使用 + golang grpc server
這篇文章主要介紹了Qt6.5 grpc組件使用+golang grpc server示例,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05使用go備份StarRocks建表語(yǔ)句方法實(shí)例
這篇文章主要為大家介紹了使用go備份StarRocks建表語(yǔ)句方法實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Go語(yǔ)言中break label與goto label的區(qū)別
這篇文章主要介紹了Go語(yǔ)言中break label與goto label的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04Go語(yǔ)言中如何實(shí)現(xiàn)并發(fā)
Go的并發(fā)機(jī)制通過(guò)協(xié)程和通道的簡(jiǎn)單性和高效性,使得編寫并發(fā)代碼變得相對(duì)容易,這種并發(fā)模型被廣泛用于構(gòu)建高性能的網(wǎng)絡(luò)服務(wù)、并行處理任務(wù)和其他需要有效利用多核處理器的應(yīng)用程序,這篇文章主要介紹了在Go中如何實(shí)現(xiàn)并發(fā),需要的朋友可以參考下2023-09-09