GO語言Context的作用及各種使用方法
Context
為什么需要Context
Go語言需要Context主要是為了在并發(fā)環(huán)境中有效地管理請(qǐng)求的上下文信息。Context提供了在函數(shù)之間傳遞取消信號(hào)、超時(shí)、截止時(shí)間等元數(shù)據(jù)的一種標(biāo)準(zhǔn)方式。
原因
- 取消操作: 在并發(fā)環(huán)境中,當(dāng)一個(gè)請(qǐng)求被取消或者超時(shí)時(shí),需要有效地通知相關(guān)的協(xié)程停止正在進(jìn)行的工作。使用Context可以通過傳遞取消信號(hào)來實(shí)現(xiàn)這一點(diǎn)。
- 超時(shí)控制: 在一些場(chǎng)景下,限制操作執(zhí)行的時(shí)間是很重要的。Context提供了一個(gè)統(tǒng)一的方式來處理超時(shí),確保在規(guī)定的時(shí)間內(nèi)完成操作,防止程序無限期地等待。
- 傳遞上下文信息: Context可以用于傳遞請(qǐng)求的元數(shù)據(jù),例如請(qǐng)求的ID、用戶信息等。這在跨多個(gè)函數(shù)調(diào)用的情況下非常有用,避免了在函數(shù)參數(shù)中傳遞大量的上下文信息。
- 協(xié)程之間的通信: Go語言中的協(xié)程(goroutine)是輕量級(jí)的線程,它們之間需要有效地通信。Context提供了一個(gè)標(biāo)準(zhǔn)的方式來傳遞信號(hào)和元數(shù)據(jù),以便協(xié)程之間協(xié)同工作。
- 資源管理: 在一些場(chǎng)景下,需要確保在函數(shù)執(zhí)行完畢后釋放相關(guān)的資源,不管函數(shù)是正常執(zhí)行還是因?yàn)槿∠虺瑫r(shí)而提前退出。Context可以幫助在正確的時(shí)機(jī)釋放資源。
- 綜上所述,Context是Go語言中處理并發(fā)、超時(shí)和取消等問題的一種優(yōu)雅而一致的方式,使得代碼更加健壯、可維護(hù),并且更容易在不同的并發(fā)場(chǎng)景中工作。
多任務(wù)超時(shí)例子
我們都知道在go語言并發(fā)編程中,我們可以采用select來監(jiān)聽協(xié)程的的通道控制協(xié)程,但是如下面的這種情況僅僅憑借select就顯得有些無能為力:
- 支持多級(jí)嵌套,父任務(wù)停止后,子任務(wù)自動(dòng)停止
- 控制停止順序,先停EFG 再停BCD 最后停A
目標(biāo)1還好說,目標(biāo)2好像就沒那么靈活了,正式討論context如何解決這些問題前,我們先看下常規(guī)context的使用
Context結(jié)構(gòu)
context 包是 Go 語言中用于處理請(qǐng)求的上下文的標(biāo)準(zhǔn)庫之一。它提供了一種在函數(shù)之間傳遞取消信號(hào)、超時(shí)和截止時(shí)間的機(jī)制。
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Deadline()
返回一個(gè)完成工作的截止時(shí)間,表示上下文應(yīng)該被取消的時(shí)間。如果 ok==false 表示沒有設(shè)置截止時(shí)間。Done()
返回一個(gè) Channel,這個(gè) Channel 會(huì)在當(dāng)前工作完成時(shí)被關(guān)閉,表示上下文應(yīng)該被取消。如果無法取消此上下文,則 Done 可能返回 nil。多次調(diào)用 Done 方法會(huì)返回同一個(gè) Channel。Err()
返回 Context 結(jié)束的原因,它只會(huì)在 Done 方法對(duì)應(yīng)的 Channel 關(guān)閉時(shí)返回非空值。如果 Context 被取消,會(huì)返回context.Canceled 錯(cuò)誤;如果 Context 超時(shí),會(huì)返回context.DeadlineExceeded錯(cuò)誤。Value()
從 Context 中獲取鍵對(duì)應(yīng)的值。如果未設(shè)置 key 對(duì)應(yīng)的值則返回 nil。以相同 key 多次調(diào)用會(huì)返回相同的結(jié)果。
另外,context 包中提供了兩個(gè)創(chuàng)建默認(rèn)上下文的函數(shù):
// TODO 返回一個(gè)非 nil 但空的上下文。 // 當(dāng)不清楚要使用哪種上下文或無可用上下文尚應(yīng)使用 context.TODO。 func TODO() Context // Background 返回一個(gè)非 nil 但空的上下文。 // 它不會(huì)被 cancel,沒有值,也沒有截止時(shí)間。它通常由 main 函數(shù)、初始化和測(cè)試使用,并作為處理請(qǐng)求的頂級(jí)上下文。 func Background() Context
還有四個(gè)基于父級(jí)創(chuàng)建不同類型上下文的函數(shù):
// WithCancel 基于父級(jí)創(chuàng)建一個(gè)具有 Done channel 的 context func WithCancel(parent Context) (Context, CancelFunc) // WithDeadline 基于父級(jí)創(chuàng)建一個(gè)不晚于 d 結(jié)束的 context func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) // WithTimeout 等同于 WithDeadline(parent, time.Now().Add(timeout)) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // WithValue 基于父級(jí)創(chuàng)建一個(gè)包含指定 key 和 value 的 context func WithValue(parent Context, key, val interface{}) Context
在后面會(huì)詳細(xì)介紹這些不同類型 context 的用法。
Context各種使用方法
創(chuàng)建context
context包主要提供了兩種方式創(chuàng)建context:
- context.Backgroud()
- context.TODO()
這兩個(gè)函數(shù)其實(shí)只是互為別名,沒有差別,官方給的定義是:
- context.Background 是上下文的默認(rèn)值,所有其他的上下文都應(yīng)該從它衍生(Derived)出來。
- context.TODO 應(yīng)該只在不確定應(yīng)該使用哪種上下文時(shí)使用;
所以在大多數(shù)情況下,我們都使用context.Background作為起始的上下文向下傳遞。
上面的兩種方式是創(chuàng)建根context,不具備任何功能,具體實(shí)踐還是要依靠context包提供的With系列函數(shù)來進(jìn)行派生:
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, val interface{}) Context
valueCtx
valueCtx結(jié)構(gòu)體
type valueCtx struct { Context key, val interface{} } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
valueCtx利用一個(gè)Context類型的變量來表示父節(jié)點(diǎn)context,所以當(dāng)前context繼承了父context的所有信息;valueCtx類型還攜帶一組鍵值對(duì),也就是說這種context可以攜帶額外的信息。valueCtx實(shí)現(xiàn)了Value方法,用以在context鏈路上獲取key對(duì)應(yīng)的值,如果當(dāng)前context上不存在需要的key,會(huì)沿著context鏈向上尋找key對(duì)應(yīng)的值,直到根節(jié)點(diǎn)。
WithValue
我們?nèi)粘T跇I(yè)務(wù)開發(fā)中都希望能有一個(gè)trace_id能串聯(lián)所有的日志,這就需要我們打印日志時(shí)能夠獲取到這個(gè)trace_id,在python中我們可以用gevent.local來傳遞,在java中我們可以用ThreadLocal來傳遞,在Go語言中我們就可以使用Context來傳遞,通過使用WithValue來創(chuàng)建一個(gè)攜帶trace_id的context,然后不斷透?jìng)飨氯?,打印日志時(shí)輸出即可,來看使用例子:
package main import ( "context" "fmt" // 我們需要使用fmt包中的Println()函數(shù) "strings" "time" "github.com/google/uuid" ) const ( KEY = "trace_id" ) // 生成隨機(jī)ID func NewRequestID() string { return strings.Replace(uuid.New().String(), "-", "", -1) } // 生成攜帶值的context func NewContextWithTraceID() context.Context { ctx := context.WithValue(context.Background(), KEY, NewRequestID()) return ctx } //打印數(shù)據(jù) func PrintLog(ctx context.Context, message string) { fmt.Printf("%s|info|trace_id=%s|%s", time.Now().Format("2006-01-02 15:04:05"), GetContextValue(ctx, KEY), message) } // 獲取context中的值 func GetContextValue(ctx context.Context, k string) string { v, ok := ctx.Value(k).(string) if !ok { return "" } return v } func ProcessEnter(ctx context.Context) { PrintLog(ctx, "Golang夢(mèng)工廠") } func main() { ProcessEnter(NewContextWithTraceID()) }
結(jié)果
2024-01-10 18:55:03|info|trace_id=c4eeb76d427449fda52a4775ccbc0509|Golang夢(mèng)工廠
cancelCtx
cancelCtx結(jié)構(gòu)體
type cancelCtx struct { Context mu sync.Mutex // 同步鎖,保護(hù)下面的所有字段 done chan struct{} //惰性創(chuàng)建,由第一次取消調(diào)用關(guān)閉 children map[canceler]struct{} // 在第一次取消調(diào)用時(shí),設(shè)置為 nil err error // 在第一次取消調(diào)用時(shí)設(shè)置為 non-nil } type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} }
跟valueCtx類似,cancelCtx中也有一個(gè)context變量作為父節(jié)點(diǎn);變量done表示一個(gè)channel,用來表示傳遞關(guān)閉信號(hào);children表示一個(gè)map,存儲(chǔ)了當(dāng)前context節(jié)點(diǎn)下的子節(jié)點(diǎn);err用于存儲(chǔ)錯(cuò)誤信息表示任務(wù)結(jié)束的原因。
withCancel
日常業(yè)務(wù)開發(fā)中我們往往為了完成一個(gè)復(fù)雜的需求會(huì)開多個(gè)gouroutine去做一些事情,這就導(dǎo)致我們會(huì)在一次請(qǐng)求中開了多個(gè)goroutine確無法控制他們,這時(shí)我們就可以使用withCancel來衍生一個(gè)context傳遞到不同的goroutine中,當(dāng)我想讓這些goroutine停止運(yùn)行,就可以調(diào)用cancel來進(jìn)行取消。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go Speak(ctx) time.Sleep(10 * time.Second) cancel() time.Sleep(1 * time.Second) } func Speak(ctx context.Context) { for range time.Tick(time.Second) { select { case <-ctx.Done(): fmt.Println("我要閉嘴了") return default: fmt.Println("balabalabalabala") } } }
timerCtx
timerCtx是一種基于cancelCtx的context類型,從字面上就能看出,這是一種可以定時(shí)取消的context。
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) cancel(removeFromParent bool, err error) { //將內(nèi)部的cancelCtx取消 c.cancelCtx.cancel(false, err) if removeFromParent { // Remove this timerCtx from its parent cancelCtx's children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { 取消計(jì)時(shí)器 c.timer.Stop() c.timer = nil } c.mu.Unlock() }
timerCtx內(nèi)部使用cancelCtx實(shí)現(xiàn)取消,另外使用定時(shí)器timer和過期時(shí)間deadline實(shí)現(xiàn)定時(shí)取消的功能。timerCtx在調(diào)用cancel方法,會(huì)先將內(nèi)部的cancelCtx取消,如果需要?jiǎng)t將自己從cancelCtx祖先節(jié)點(diǎn)上移除,最后取消計(jì)時(shí)器。
WithDeadline
WithDeadline 用于設(shè)置一個(gè)絕對(duì)時(shí)間,表示在某個(gè)具體的時(shí)間點(diǎn)超時(shí),例如 context.WithDeadline(parentContext, time.Now().Add(10 * time.Second)) 表示在當(dāng)前時(shí)間的 10 秒后超時(shí)。
package main import ( "context" "fmt" "time" ) func main() { HttpHandler() } func NewContextWithTimeout() (context.Context, context.CancelFunc) { return context.WithDeadline(context.Background(), time.Now().Add(10*time.Second)) } func HttpHandler() { ctx, cancel := NewContextWithTimeout() defer cancel() deal(ctx) } func deal(ctx context.Context) { for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default: fmt.Printf("deal time is %d\n", i) } } }
WithTimeout
WithTimeout 用于設(shè)置一個(gè)相對(duì)時(shí)間,表示在多長時(shí)間后超時(shí),例如 context.WithTimeout(parentContext, 5 * time.Second) 表示在 5 秒后超時(shí)。
package main import ( "context" "fmt" "time" ) func main() { HttpHandler() } func NewContextWithTimeout() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 3*time.Second) } func HttpHandler() { ctx, cancel := NewContextWithTimeout() defer cancel() deal(ctx) } func deal(ctx context.Context) { for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println(ctx.Err()) return default: fmt.Printf("deal time is %d\n", i) } } }
總結(jié)
context主要用于父子任務(wù)之間的同步取消信號(hào),本質(zhì)上是一種協(xié)程調(diào)度的方式。另外在使用context時(shí)有兩點(diǎn)值得注意:上游任務(wù)僅僅使用context通知下游任務(wù)不再需要,但不會(huì)直接干涉和中斷下游任務(wù)的執(zhí)行,由下游任務(wù)自行決定后續(xù)的處理操作,也就是說context的取消操作是無侵入的;context是線程安全的,因?yàn)閏ontext本身是不可變的(immutable),因此可以放心地在多個(gè)協(xié)程中傳遞使用。
到此這篇關(guān)于GO語言Context的作用及各種使用方法的文章就介紹到這了,更多相關(guān)GO語言Context使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言中init函數(shù)和defer延遲調(diào)用關(guān)鍵詞詳解
這篇文章主要介紹了Go語言中init函數(shù)和defer延遲調(diào)用關(guān)鍵詞,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03Golang如何編寫內(nèi)存高效及CPU調(diào)優(yōu)的Go結(jié)構(gòu)體
這篇文章主要介紹了Golang如何編寫內(nèi)存高效及CPU調(diào)優(yōu)的Go結(jié)構(gòu)體,結(jié)構(gòu)體是包含多個(gè)字段的集合類型,用于將數(shù)據(jù)組合為記錄2022-07-07go語言中嵌套結(jié)構(gòu)體的實(shí)現(xiàn)
在Go語言中,嵌套結(jié)構(gòu)體可定義為一個(gè)結(jié)構(gòu)體內(nèi)包含另一個(gè)結(jié)構(gòu)體,嵌套可以是值嵌套或指針嵌套,兩者在內(nèi)存分配和修改影響上有顯著區(qū)別,本文就來詳細(xì)的介紹一下,感興趣的可以了解一下2024-09-09Go語言crypto包創(chuàng)建自己的密碼加密工具實(shí)現(xiàn)示例
Go語言借助它的簡(jiǎn)單性和強(qiáng)大的標(biāo)準(zhǔn)庫,實(shí)現(xiàn)一個(gè)自己的密碼加密工具,本文將會(huì)結(jié)合代碼示例深入探討如何使用Go語言的crypto包來實(shí)現(xiàn)自己的加密工具2023-11-11基于Go語言簡(jiǎn)單實(shí)現(xiàn)事件管理器
在編程中,事件管理器是一種常見的工具,用于通過通知來觸發(fā)操作,本文將介紹一個(gè)簡(jiǎn)單的Go事件管理器的實(shí)現(xiàn),并通過異步改進(jìn)提高其性能,感興趣的可以了解下2023-11-11Go?channel實(shí)現(xiàn)批量讀取數(shù)據(jù)
Go中的?channel?其實(shí)并沒有提供批量讀取數(shù)據(jù)的方法,需要我們自己實(shí)現(xiàn)一個(gè),使用本文就來為大家大家介紹一下如何通過Go?channel實(shí)現(xiàn)批量讀取數(shù)據(jù)吧2023-12-12