context.Context是Go中定義的一個(gè)接口類型,從1.7版本中開始引入。其主要作用是在一次請(qǐng)求經(jīng)過的所有協(xié)程或函數(shù)間傳遞取消信號(hào)及共享數(shù)據(jù),以達(dá)到父協(xié)程對(duì)子協(xié)程的管理和控制的目的。
需要注意的是context.Context的作用范圍是一次請(qǐng)求的生命周期,即隨著請(qǐng)求的產(chǎn)生而產(chǎn)生,隨著本次請(qǐng)求的結(jié)束而結(jié)束。如圖所示:
在context包中,我們看到context.Context的定義實(shí)際上是一個(gè)接口類型,該接口定義了獲取上下文的Deadline的函數(shù),根據(jù)key獲取value值的函數(shù)、還有獲取done通道的函數(shù)。如下:
(資料圖片僅供參考)
typeContextinterface{Deadline()(deadlinetime.Time,okbool)Done()<-chanstruct{}Err()error Value(key interface{}) interface{}}
由定義的接口函數(shù)可知,對(duì)于傳遞取消信號(hào)的行為我們可以描述為:當(dāng)協(xié)程運(yùn)行時(shí)間達(dá)到Deadline時(shí),就會(huì)調(diào)用取消函數(shù),關(guān)閉done通道,往done通道中輸入一個(gè)空結(jié)構(gòu)體消息struct{}{},這時(shí)所有監(jiān)聽done通道的子協(xié)程都會(huì)收到該消息,便知道父協(xié)程已經(jīng)關(guān)閉,需要自己也結(jié)束運(yùn)行。
下面是一個(gè)使用Context的簡(jiǎn)易示例,我們通過該示例來說明父子協(xié)程之間是如何傳遞取消信號(hào)的。
func main() { ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) defer cancel() go doSomethingCool(ctx) select { case <-ctx.Done(): fmt.Println("oh no, I"ve exceeded the deadline") }}func doSomethingCool(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("timed out") return default: fmt.Println("doing something cool") } time.Sleep(500 * time.Millisecond) }}
由示例可知,main協(xié)程和doSomething函數(shù)之間的唯一關(guān)聯(lián)就是ctx.Done()。當(dāng)子協(xié)程從ctx.Done()通道中接收到輸出時(shí)(因?yàn)槌瑫r(shí)自動(dòng)取消或主動(dòng)調(diào)用了cancel函數(shù)),即認(rèn)為是父協(xié)程不再需要子協(xié)程返回的結(jié)果了,子協(xié)程就會(huì)直接返回,不再執(zhí)行其他的邏輯。
3.1 如何創(chuàng)建帶可以傳遞信號(hào)的Context
在開頭處我們得知Context本質(zhì)是一個(gè)接口類型。接口類型是需要具體的結(jié)構(gòu)體起來實(shí)現(xiàn)的。那我們需要自定義結(jié)構(gòu)體類型來實(shí)現(xiàn)這些接口嗎?答案是不需要。因?yàn)樵赾ontext包中已經(jīng)定義好了所需場(chǎng)景的結(jié)構(gòu)體,這些結(jié)構(gòu)體已經(jīng)幫我們實(shí)現(xiàn)了Context接口的方法,在項(xiàng)目中就已經(jīng)夠用了。
在context包中定義有emptyCtx、cancelCtx、timerCtx、valueCtx
四種結(jié)構(gòu)體。其中cancelCtx、timerCtx實(shí)現(xiàn)了給子協(xié)程傳遞取消信號(hào)。valueCtx結(jié)構(gòu)體實(shí)現(xiàn)了父協(xié)程和子協(xié)程傳遞共享數(shù)據(jù)相關(guān)。本節(jié)我們重點(diǎn)來看跟傳遞信號(hào)相關(guān)的Context。
在上面示例中,我們通過context.WithTimeout函數(shù)創(chuàng)建了一個(gè)帶定時(shí)取消功能的Context實(shí)例,該示例本質(zhì)上是創(chuàng)建了一個(gè)timerCtx結(jié)構(gòu)體的實(shí)例。在context包中還有WithCancel、WithDeadline函數(shù)也可以創(chuàng)建對(duì)應(yīng)的結(jié)構(gòu)體,其定義如下:
//創(chuàng)建帶有取消功能的Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc) //創(chuàng)建帶有定時(shí)自動(dòng)取消功能的Contextfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)//創(chuàng)建帶有定時(shí)自動(dòng)取消功能的Contextfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
對(duì)應(yīng)的函數(shù)創(chuàng)建的結(jié)構(gòu)體及該實(shí)例所實(shí)現(xiàn)的功能的主要特點(diǎn)如下圖所示:
在圖中我們看到結(jié)構(gòu)體依次是繼承關(guān)系。因?yàn)樵赾ancelCtx結(jié)構(gòu)體內(nèi)嵌套了Context(實(shí)際上是emptyCtx)、timerCtx結(jié)構(gòu)體內(nèi)嵌套了cancelCtx結(jié)構(gòu)體,可以認(rèn)為他們之間存在繼承關(guān)系。
通過WithTimeout和WithDealine函數(shù)創(chuàng)建的Context實(shí)際上都是timerCtx結(jié)構(gòu)體,唯一的區(qū)別就是WithDeadline函數(shù)的第二個(gè)參數(shù)指定的是最后的時(shí)間點(diǎn),而WithTimeout函數(shù)的第二個(gè)參數(shù)是一段時(shí)間。但WithDealine在內(nèi)部實(shí)現(xiàn)中本質(zhì)上也是將時(shí)間點(diǎn)轉(zhuǎn)換成距離當(dāng)前的時(shí)間段。
3.2 為什么Done函數(shù)返回值是通道
在Context接口的定義中我們看到Done函數(shù)的定義,其返回值是一個(gè)輸出通道:
Done() <-chan struct{}
在上面的示例中我們看到的子協(xié)程是通過監(jiān)聽Context的Done()函數(shù)返回的通道來判斷父協(xié)程是否發(fā)送了取消信號(hào)的。當(dāng)父協(xié)程調(diào)用取消函數(shù)時(shí),該取消函數(shù)將該通道關(guān)閉。關(guān)閉通道相當(dāng)于是一個(gè)廣播信息,當(dāng)監(jiān)聽該通道的接收者從通道到中接收完最后一個(gè)元素后,接收者都會(huì)解除阻塞,并從通道中接收到通道元素類型的零值。
既然父子協(xié)程是通過通道傳到信號(hào)的。下面我們介紹父協(xié)程是如何將信號(hào)通過通道傳遞給子協(xié)程的。
3.3 父協(xié)程是如何取消子協(xié)程的
我們發(fā)現(xiàn)在Context接口中并沒有定義Cancel方法。實(shí)際上通過WithCancel函數(shù)創(chuàng)建的一個(gè)具有可取消功能的Context實(shí)例來實(shí)現(xiàn)的:
// WithCancel returns a copy of parent whose Done channel is closed as soon as// parent.Done is closed or cancel is called.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) }}
WithCancel函數(shù)的返回值有兩個(gè),一個(gè)是ctx,一個(gè)是取消函數(shù)cancel。當(dāng)父協(xié)程調(diào)用cancel函數(shù)時(shí),就相當(dāng)于觸發(fā)了關(guān)閉的動(dòng)作,在cancel的執(zhí)行邏輯中會(huì)將ctx的done通道關(guān)閉,然后所有監(jiān)聽該通道的子協(xié)程就會(huì)收到一個(gè)struct{}類型的零值,子協(xié)程根據(jù)此便執(zhí)行了返回操作。下面是cancel函數(shù)實(shí)現(xiàn):
// cancel closes c.done, cancels each of c"s children, and, if// removeFromParent is true, removes c from its parent"s children.func (c *cancelCtx) cancel(removeFromParent bool, err error) { //... d, _ := c.done.Load().(chan struct{})//獲取通道 if d == nil { c.done.Store(closedchan) } else { close(d) //關(guān)閉通道done } //...}
由源碼可知,cancelCtx的cancel函數(shù)執(zhí)行時(shí)會(huì)關(guān)閉通道close(d)。
通過WithCancel函數(shù)構(gòu)造的Context,需要開發(fā)者自己設(shè)定調(diào)用取消函數(shù)的條件。而在某些場(chǎng)景下需要設(shè)定超時(shí)時(shí)間,比如調(diào)用grpc服務(wù)時(shí)設(shè)置超時(shí)時(shí)間,那么實(shí)際上就是在構(gòu)造Context的同時(shí),啟動(dòng)一個(gè)定時(shí)任務(wù),當(dāng)達(dá)到設(shè)定的定時(shí)時(shí)間時(shí),就自動(dòng)調(diào)用cancel函數(shù)即可。這就是context包中提供的WithDeadline和WithTimeout函數(shù)來構(gòu)造的上下文。如下是WithDeadline函數(shù)的關(guān)鍵實(shí)現(xiàn)部分:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { //... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) //... if c.err == nil { //這里實(shí)現(xiàn)定時(shí)器,即dur時(shí)間后執(zhí)行cancel函數(shù) c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
WithTimeout函數(shù)也是將相對(duì)時(shí)間timeout轉(zhuǎn)換成絕對(duì)的時(shí)間點(diǎn)deadline之后,調(diào)用的WithDeadline函數(shù)。
3.4為什么要通過WithXXX函數(shù)構(gòu)造一個(gè)樹形結(jié)構(gòu)
很多文章都說,通過WithXXX函數(shù)基于Context會(huì)衍生出一個(gè)Context樹,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn)Context。如下圖表示:
那為什么要構(gòu)造一個(gè)樹形結(jié)構(gòu)呢?我們從處理一個(gè)請(qǐng)求時(shí)經(jīng)過的多個(gè)協(xié)程來角度來理解會(huì)更容易一些。當(dāng)一個(gè)請(qǐng)求到來時(shí),該請(qǐng)求會(huì)經(jīng)過很多個(gè)協(xié)程的處理,而這些協(xié)程之間的關(guān)系實(shí)際上就組成了一個(gè)樹形結(jié)構(gòu)。如下圖:
Context的目的就是為了在關(guān)聯(lián)的協(xié)程間傳遞信號(hào)和共享數(shù)據(jù)的,而每個(gè)協(xié)程又只能管理自己的子節(jié)點(diǎn),而不能管理父節(jié)點(diǎn)。所以,在整個(gè)處理過程中,Context自然就衍生成了樹形結(jié)構(gòu)。
3.5為什么WithXXX函數(shù)返回的是一個(gè)新的Context對(duì)象
通過WithXXX的源碼可以看到,每個(gè)衍生函數(shù)返回來的都是一個(gè)新的Context對(duì)象,并且都是基于parent Context的。以WithDeadline為例,就是返回的一個(gè)timerCtx新的結(jié)構(gòu)體實(shí)例。這是因?yàn)?,在Context的傳遞過程中,每個(gè)協(xié)程都能根據(jù)自己的需要來定制Context(例如,在上圖中,main協(xié)程調(diào)用goroutine2時(shí)要求是600毫秒完成操作,但goroutine2調(diào)用goroutine2.1時(shí),要求是500毫秒內(nèi)完成操作),而這些修改又不能影響之前已經(jīng)調(diào)用的函數(shù),只能對(duì)向下傳遞。所以,通過一個(gè)新的Context值來進(jìn)行傳遞。
Context的另外一個(gè)功能就是在協(xié)程間共享數(shù)據(jù)。該功能是通過WithValue函數(shù)構(gòu)造的Context來實(shí)現(xiàn)的。我們看下WithValue的實(shí)現(xiàn):
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}}
實(shí)現(xiàn)代碼很簡(jiǎn)短,我們看到最終返回的是一個(gè)valueCtx結(jié)構(gòu)體實(shí)例。其中有兩點(diǎn):一是key的類型必須是可比較的。二是value是不能修改的,即具有不可變性。如果需要添加新的值,只能通過WithValue基于原有的Context再生成一個(gè)新的valueCtx來攜帶新的key-value。這也是Context的值在傳遞過程中是并發(fā)安全的原因。從另外一個(gè)角度來說,在獲取一個(gè)key的值的時(shí)候,也是遞歸的一層一層的從下往上查找,如下:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
上面簡(jiǎn)單介紹了下在協(xié)程間調(diào)用的時(shí)候是如何通過Context共享數(shù)據(jù)的。
但這里討論的重點(diǎn)是什么樣的數(shù)據(jù)需要通過Context來共享,而不是通過傳參的方式。總結(jié)下來有以下兩點(diǎn):
攜帶的數(shù)據(jù)作用域必須是在請(qǐng)求范圍內(nèi)有效的。即該數(shù)據(jù)隨著請(qǐng)求的產(chǎn)生而產(chǎn)生,隨著請(qǐng)求的結(jié)束而結(jié)束,不會(huì)永久的保存。攜帶的數(shù)據(jù)不建議是關(guān)鍵參數(shù),關(guān)鍵參數(shù)應(yīng)顯式的通過參數(shù)來傳遞。例如像trace_id之類的,用于維護(hù)作用,就適合用在Context中傳遞。4.1 什么是請(qǐng)求范圍(request-scoped)內(nèi)的數(shù)據(jù)
這個(gè)沒有一個(gè)明顯的劃定標(biāo)準(zhǔn)。一般的請(qǐng)求范圍的數(shù)據(jù)就是用來表示該請(qǐng)求的元數(shù)據(jù)。比如該請(qǐng)求是由誰發(fā)出(即user id),該請(qǐng)求是在哪兒發(fā)出的(即user ip,請(qǐng)求是從該用戶的ip位置發(fā)出的)。
例如,如果一個(gè)日志對(duì)象logger是一個(gè)單例那么它也不是一個(gè)請(qǐng)求范圍內(nèi)的數(shù)據(jù)。但如果該logger包含了發(fā)送請(qǐng)求的來源信息,以及該請(qǐng)求是否啟動(dòng)了調(diào)試功能的開關(guān)信息,那么該logger也可以被認(rèn)為是一個(gè)請(qǐng)求范圍內(nèi)的數(shù)據(jù)。
4.2 使用Context.Value的缺點(diǎn)
使用Context.Value會(huì)對(duì)降低函數(shù)的可讀性和表達(dá)性。例如,下面是使用Context.Value來攜帶token驗(yàn)證角色的示例:
func IsAdminUser(ctx context.Context) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
當(dāng)用戶調(diào)用該函數(shù)的時(shí)候,僅僅知道該函數(shù)帶有一個(gè)Context類型的參數(shù)。但如果要判斷一個(gè)用戶是否是Admin必須要兩部分要說明:一個(gè)是驗(yàn)證過的token,一個(gè)是認(rèn)證服務(wù)。
我們將該函數(shù)的Context移除,然后使用參數(shù)的方式來重構(gòu),如下:
func IsAdminUser(token string, authService AuthService) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
那么這個(gè)函數(shù)的可讀性和表達(dá)性就比重構(gòu)前提高了很多。調(diào)用者通過函數(shù)簽名就很容易知道要判斷一個(gè)用戶是否是AdminUser,只需要傳入token和認(rèn)證的服務(wù)authService即可。
4.3 context.Value的使用場(chǎng)景
一般復(fù)雜的項(xiàng)目都會(huì)有中間件層以及大量的抽象層。如果將類似token或userid這樣簡(jiǎn)單的參數(shù)以參數(shù)的方式從第一個(gè)函數(shù)層層傳遞,那對(duì)調(diào)用者來說將會(huì)是一種噩夢(mèng)。如果將這樣的元數(shù)據(jù)通過Context來攜帶進(jìn)行傳遞,將會(huì)是比較好的方式。在實(shí)際項(xiàng)目中,最常用的就是在中間件中。我們以iris為web框架,來看下在中間件中的應(yīng)用:
package mainimport ( "context" "github.com/google/uuid" "github.com/kataras/iris/v12")func main() { app := iris.New() app.Use(RequestIDMiddleware) app.Get("/hello", mainHandler) app.Listen("localhost:8080", iris.WithOptimizations)}func RequestIDMiddleware(c iris.Context) { reqID := uuid.New() ctx := context.WithValue(c.Request().Context(), "req_id", reqID) req := c.Request().Clone(ctx) c.ResetRequest(req) c.Next()}func mainHandler(ctx iris.Context) { req_id := ctx.Request().Context().Value("req_id") ctx.Writef("Hello request id:%s", req_id) return}
context包是go語言中的一個(gè)重要的特性。要想正確的在項(xiàng)目中使用context,理解其背后的工作機(jī)制以及設(shè)計(jì)意圖是非常重要的。context包定義了一個(gè)API,它提供對(duì)截止日期、取消信號(hào)和請(qǐng)求范圍值的支持,這些值可以跨API以及在Goroutine之間傳遞。
關(guān)鍵詞:
凡本網(wǎng)注明“XXX(非中國微山網(wǎng))提供”的作品,均轉(zhuǎn)載自其它媒體,轉(zhuǎn)載目的在于傳遞更多信息,并不代表本網(wǎng)贊同其觀點(diǎn)和其真實(shí)性負(fù)責(zé)。
李敏鎬的兵役結(jié)束了。李敏鎬2017年5月12日,入伍服役。2019年4月25
2023-03-15 11:10
艾滋病病源首次確認(rèn)及防護(hù)注意事項(xiàng),據(jù)外媒報(bào)道,多國科學(xué)家研究發(fā)現(xiàn),艾滋病毒已知的4種病株,均來自喀麥隆的黑猩猩及大猩猩,是人類首次完全
2023-03-15 11:13
和網(wǎng)友約會(huì)的注意事項(xiàng),隨著網(wǎng)絡(luò)越來越發(fā)達(dá),很多人都通過網(wǎng)上認(rèn)識(shí)?,F(xiàn)實(shí)中可能聊不到一起,但是在網(wǎng)上兩人互相不認(rèn)識(shí)的人卻有很多話題,時(shí)間長(zhǎng)
2023-03-15 11:28
戰(zhàn)艦少女R5-3通關(guān)攻略,戰(zhàn)艦少女R5-3通關(guān)攻略,5-3的BOSS點(diǎn)可以出大黃蜂,特產(chǎn)是B25轟炸機(jī),萌新經(jīng)常要打撈的圖,下面我們就來看看5-3的攻略把
2023-03-15 11:14
出自《般若波羅密多心經(jīng)》。這句話的意思就是什么事情你都不要執(zhí)著
2023-03-15 11:16
太空生活有哪些趣事?1、在宇宙飛船里,站著睡覺和躺著睡覺一樣舒服
2023-03-15 11:30
沒有風(fēng)欲靜而樹不止,子欲養(yǎng)而親不待這句話,應(yīng)該是樹欲靜而風(fēng)不止
2023-03-15 11:09
新華美育學(xué)生注冊(cè)登錄該怎么登錄,新華美育學(xué)生注冊(cè)登錄該怎么登錄
2023-03-15 11:28
英雄聯(lián)盟手游怎樣獲取探戈靈魂崔斯特,在英雄聯(lián)盟手游中,探戈靈魂崔斯特是一款非常炫酷的皮膚,那么怎樣獲得它呢,下面小編就和大家分享一下
2023-03-15 11:10
MyEclipse2013安裝指南,MyEclie2013是一款插件豐富的Java開發(fā)工具,使用起來非常方便。以下是MyEclie2013安裝步驟和詳解。盡量默認(rèn)安裝,避免
2023-03-15 11:22