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