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