一文帶你掌握Go語言并發(fā)模式中的Context的上下文管理
前言
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
Go
在 1.7 引入了 context
包,目的是為了在不同的 goroutine
之間或跨 API
邊界傳遞超時、取消信號和其他請求范圍內(nèi)的值(與該請求相關(guān)的值。這些值可能包括用戶身份信息、請求處理日志、跟蹤信息等等)。
在 Go
的日常開發(fā)中,Context
上下文對象無處不在,無論是處理網(wǎng)絡(luò)請求、數(shù)據(jù)庫操作還是調(diào)用 RPC
等場景下,都會使用到 Context
。那么,你真的了解它嗎?熟悉它的正確用法嗎?了解它的使用注意事項嗎?喝一杯你最喜歡的飲料,隨著本文一探究竟吧。
Context 接口
context
包在提供了一個用于跨 API
邊界傳遞超時、取消信號和其他請求范圍值的通用數(shù)據(jù)結(jié)構(gòu)。它定義了一個名為 Context
的接口,該接口包含一些方法,用于在多個 Goroutine
和函數(shù)之間傳遞請求范圍內(nèi)的信息。
以下是 Context
接口的定義:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chanstruct{} Err() error Value(key any) any }
Context 的核心方法
Context 的核心方法.jpg
Context
接口中有四個核心方法:Deadline()
、Done()
、Err()
、Value()
。
Deadline()
Deadline() (deadline time.Time, ok bool)
方法返回 Context
的截止時間,表示在這個時間點之后,Context
會被自動取消。如果 Context
沒有設(shè)置截止時間,該方法返回一個零值 time.Time
和一個布爾值 false
。
deadline, ok := ctx.Deadline() if ok { // Context 有截止時間 } else { // Context 沒有截止時間 }
Done()
Done()
方法返回一個只讀通道,當(dāng) Context
被取消時,該通道會被關(guān)閉。你可以通過監(jiān)聽這個通道來檢測 Context
是否被取消。如果 Context
永不取消,則返回 nil
。
select { case <-ctx.Done(): // Context 已取消 default: // Context 尚未取消 }
Err()
Err()
方法返回一個 error
值,表示 Context
被取消時產(chǎn)生的錯誤。如果 Context
尚未取消,該方法返回 nil
。
if err := ctx.Err(); err != nil { // Context 已取消,處理錯誤 }
Value()
Value(key any) any
方法返回與 Context
關(guān)聯(lián)的鍵值對,一般用于在 Goroutine
之間傳遞請求范圍內(nèi)的信息。如果沒有關(guān)聯(lián)的值,則返回 nil
。
value := ctx.Value(key) if value != nil { // 存在關(guān)聯(lián)的值 }
Context 的創(chuàng)建方式
Context 的創(chuàng)建方式.jpg
context.Background()
context.Background()
函數(shù)返回一個非 nil
的空 Context
,它沒有攜帶任何的值,也沒有取消和超時信號。通常作為根 Context
使用。
ctx := context.Background()
context.TODO()
context.TODO()
函數(shù)返回一個非 nil
的空 Context
,它沒有攜帶任何的值,也沒有取消和超時信號。雖然它的返回結(jié)果和 context.Background()
函數(shù)一樣,但是它們的使用場景是不一樣的,如果不確定使用哪個上下文時,可以使用 context.TODO()
。
ctx := context.TODO()
context.WithValue()
context.WithValue(parent Context, key, val any)
函數(shù)接收一個父 Context
和一個鍵值對 key
、val
,返回一個新的子 Context
,并在其中添加一個 key-value
數(shù)據(jù)對。
ctx := context.WithValue(parentCtx, "username", "陳明勇")
context.WithCancel()
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
函數(shù)接收一個父 Context
,返回一個新的子 Context
和一個取消函數(shù),當(dāng)取消函數(shù)被調(diào)用時,子 Context
會被取消,同時會向子 Context
關(guān)聯(lián)的 Done()
通道發(fā)送取消信號,屆時其衍生的子孫 Context
都會被取消。這個函數(shù)適用于手動取消操作的場景。
ctx, cancelFunc := context.WithCancel(parentCtx) defer cancelFunc()
context.WithCancelCause() 與 context.Cause()
context.WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
函數(shù)是 Go 1.20
版本才新增的,其功能類似于 context.WithCancel()
,但是它可以設(shè)置額外的取消原因,也就是 error
信息,返回的 cancel
函數(shù)被調(diào)用時,需傳入一個 error
參數(shù)。
ctx, cancelFunc := context.WithCancelCause(parentCtx) defer cancelFunc(errors.New("原因"))
context.Cause(c Context) error
函數(shù)用于返回取消 Context
的原因,即錯誤值 error
。如果是通過 context.WithCancelCause()
函數(shù)返回的取消函數(shù) cancelFunc(myErr)
進行的取消操作,我們可以獲取到 myErr
的值。否則,我們將得到與 c.Err()
相同的返回值。如果 Context
尚未被取消,將返回 nil
。
err := context.Cause(ctx)
context.WithDeadline()
context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
函數(shù)接收一個父 Context
和一個截止時間作為參數(shù),返回一個新的子 Context
。當(dāng)截止時間到達時,子 Context
其衍生的子孫 Context
會被自動取消。這個函數(shù)適用于需要在特定時間點取消操作的場景。
deadline := time.Now().Add(time.Second * 2) ctx, cancelFunc := context.WithTimeout(parentCtx, deadline) defer cancelFunc()
context.WithTimeout()
context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
函數(shù)和 context.WithDeadline()
函數(shù)的功能是一樣的,其底層會調(diào)用 WithDeadline()
函數(shù),只不過其第二個參數(shù)接收的是一個超時時間,而不是截止時間。這個函數(shù)適用于需要在一段時間后取消操作的場景。
ctx, cancelFunc := context.WithTimeout(parentCtx, time.Second * 2) defer cancelFunc()
Context 的使用場景
傳遞共享數(shù)據(jù)
編寫中間件函數(shù),用于向 HTTP
處理鏈中添加處理請求 ID
的功能。
type key int const ( requestIDKey key = iota ) func WithRequestId(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { // 從請求中提取請求ID和用戶信息 requestID := req.Header.Get("X-Request-ID") // 創(chuàng)建子 context,并添加一個請求 Id 的信息 ctx := context.WithValue(req.Context(), requestIDKey, requestID) // 創(chuàng)建一個新的請求,設(shè)置新 ctx req = req.WithContext(ctx) // 將帶有請求 ID 的上下文傳遞給下一個處理器 next.ServeHTTP(rw, req) }) }
首先,我們從請求的頭部中提取請求 ID
。然后使用 context.WithValue
創(chuàng)建一個子上下文,并將請求 ID
作為鍵值對存儲在子上下文中。接著,我們創(chuàng)建一個新的請求對象,并將子上下文設(shè)置為新請求的上下文。最后,我們將帶有請求 ID
的上下文傳遞給下一個處理器。這樣,通過使用 WithRequestId
中間件函數(shù),我們可以在處理請求的過程中方便地獲取和使用請求 ID
,例如在 日志記錄、跟蹤和調(diào)試等方面。
傳遞取消信號,結(jié)束任務(wù)
啟動一個工作協(xié)程,接收到取消信號就停止工作。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancelFunc := context.WithCancel(context.Background()) go Working(ctx) time.Sleep(3 * time.Second) cancelFunc() // 等待一段時間,以確保工作協(xié)程接收到取消信號并退出 time.Sleep(1 * time.Second) } func Working(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("下班啦...") return default: fmt.Println("陳明勇正在工作中...") } } }
執(zhí)行結(jié)果
······
······
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
陳明勇正在工作中...
下班啦...
在上面的示例中,我們創(chuàng)建了一個 Working
函數(shù),它會不斷執(zhí)行工作任務(wù)。我們使用 context.WithCancel
創(chuàng)建了一個上下文 ctx
和一個取消函數(shù) cancelFunc
。然后,啟動了一個工作協(xié)程,并將上下文傳遞給它。
在主函數(shù)中,需要等待一段時間(3 秒)模擬業(yè)務(wù)邏輯的執(zhí)行。然后,調(diào)用取消函數(shù) cancelFunc
,通知工作協(xié)程停止工作。工作協(xié)程在每次循環(huán)中都會檢查上下文的狀態(tài),一旦接收到取消信號,就會退出循環(huán)。
最后,等待一段時間(1 秒),以確保工作協(xié)程接收到取消信號并退出。
超時控制
模擬耗時操作,超時控制。
package main import ( "context" "fmt" "time" ) func main() { // 使用 WithTimeout 創(chuàng)建一個帶有超時的上下文對象 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // 在另一個 goroutine 中執(zhí)行耗時操作 gofunc() { // 模擬一個耗時的操作,例如數(shù)據(jù)庫查詢 time.Sleep(5 * time.Second) cancel() }() select { case <-ctx.Done(): fmt.Println("操作已超時") case <-time.After(10 * time.Second): fmt.Println("操作完成") } }
執(zhí)行結(jié)果
操作已超時
在上面的例子中,首先使用 context.WithTimeout()
創(chuàng)建了一個帶有 3 秒超時的上下文對象 ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
。
接下來,在一個新的 goroutine
中執(zhí)行一個模擬的耗時操作,例如等待 5 秒鐘。當(dāng)耗時操作完成后,調(diào)用 cancel()
方法來取消超時上下文。
最后,在主 goroutine
中使用 select
語句等待超時上下文的完成信號。如果在 3 秒內(nèi)耗時操作完成,那么會輸出 "操作完成"。如果超過了 3 秒仍未完成,超時上下文的 Done()
通道會被關(guān)閉,輸出 "操作已超時"。
使用 Context 的一些規(guī)則
使用 Context
上下文,應(yīng)該遵循以下規(guī)則,以保持包之間的接口一致,并使靜態(tài)分析工具能夠檢查上下文傳播:
不要在結(jié)構(gòu)類型中加入 Context
參數(shù),而是將它顯式地傳遞給需要它的每個函數(shù),并且它應(yīng)該是第一個參數(shù),通常命名為 ctx
:
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
即使函數(shù)允許,也不要傳遞 nil Context
。如果不確定要使用哪個 Context
,建議使用 context.TODO()
。
僅將 Context
的值用于傳輸進程和 api
的請求作用域數(shù)據(jù),不能用于向函數(shù)傳遞可選參數(shù)。
小結(jié)
本文詳細(xì)介紹了 Go
語言中的 Context
上下文,通過閱讀本文,相信你們對 Context
的功能和使用場景有所了解。同時,你們也應(yīng)該能夠根據(jù)實際需求選擇最合適的 Context
創(chuàng)建方式,并且根據(jù)規(guī)則,正確、高效地使用它。
以上就是一文帶你掌握Go語言并發(fā)模式中的Context的上下文管理的詳細(xì)內(nèi)容,更多關(guān)于Go語言Context的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go Excelize API源碼解讀GetSheetViewOptions與SetPageLayo
這篇文章主要為大家介紹了Go Excelize API源碼解讀GetSheetViewOptions與SetPageLayout方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08golang?pprof?監(jiān)控系列?go?trace統(tǒng)計原理與使用解析
這篇文章主要為大家介紹了golang?pprof?監(jiān)控系列?go?trace統(tǒng)計原理與使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04詳解Golang實現(xiàn)http重定向https的方式
這篇文章主要介紹了詳解Golang實現(xiàn)http重定向https的方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08Go語言學(xué)習(xí)筆記之golang操作MongoDB數(shù)據(jù)庫
MongoDB是Nosql中常用的一種數(shù)據(jù)庫,這篇文章主要給大家介紹了關(guān)于Go語言學(xué)習(xí)筆記之golang操作MongoDB數(shù)據(jù)庫的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05