網站首頁 編程語言 正文
Context 背景 和 適用場景
Context 的背景
Golang 在 1.6.2 的時候還沒有自己的 context,在1.7的版本中就把 https://pkg.go.dev/golang.org/x/net/context包被加入到了官方的庫中。Golang 的 Context 包,中文可以稱之為“上下文”,是用來在 goroutine 協程之間進行上下文信息傳遞的,這些上下文信息包括 kv 數據、取消信號、超時時間、截止時間等。
Context 的功能和目的
雖然我們知道了 context 上下文的基本信息,但是想想,為何 Go 里面把 Context 單獨擰出來設計呢?這就和 Go 的并發有比較大的關系,因為 Go 里面創建并發協程非常容易,但是,如果沒有相關的機制去控制這些這些協程的生命周期,那么可能導致協程泛濫,也可能導致請求大量超時,協程無法退出導致協程泄漏、協程泄漏導致協程占用的資源無法釋放,從而導致資源被占滿等各種問題。所以,context 出現的目的就是為了解決并發協程之間父子進程的退出控制。
一個常見例子,有一個 web 服務器,來一個請求,開多個協程去處理這個請求的業務邏輯,比如,查詢登錄狀態、獲取用戶信息、獲取業務信息等,那么如果請求的下游協程的生命周期無法控制,那么我們的業務請求就可能會一直超時,業務服務可能會因為協程沒有釋放導致協程泄漏。因此,協程之間能夠進行事件通知并且能控制協程的生命周期非常重要,怎么實現呢? context 就是來干這些事的。
另外,既然有大量并發協程,那么各個協程之間的一些基礎數據如果想要共享,比如把每個請求鏈路的 tarceID 都進行傳遞,這樣把整個鏈路串起來,要怎么做呢? 還是要依靠 context。
總體來說,context 的目的主要包括兩個:
- 協程之間的事件通知(超時、取消)
- 協程之間的數據傳遞鍵值對的數據(kv 數據)
Context 的基本使用
Go 語言中的 Context 直接使用官方的 "context"
包就可以開始使用了,一般是在我們所有要傳遞的地方(函數的第一個參數)把 context.Context 類型的變量傳遞,并對其進行相關 API 的使用。context 常用的使用姿勢包括但不限于:
- 通過 context 進行數據傳遞,但是這里只能傳遞一些通用或者基礎的元數據,不要傳遞業務層面的數據,不是說不可以傳遞,是在 Go 的編碼規范或者慣用法中不提倡
- 通過 context 進行協程的超時控制
- 通過 context 進行并發控制
Context 的同步控制設計
Go 里面控制并發有兩種經典的方式,一種是 WaitGroup,另外一種就是 Context。
在 Go 里面,當需要進行多批次的計算任務同步,或者需要一對多的協作流程的時候;通過 Context 的關聯關系(go 的 context 被設計為包含了父子關系),我們就可以控制子協程的生命周期,而其他的同步方式是無法控制其生命周期的,只能是被動阻塞等待完成或者結束。context 控制子協程的生命周期,是通過 context 的 context.WithTimeout 機制來實現的,這個是一般系統中或者底層各種框架、庫的普適用法。context 對并發做一些控制包括 Context Done 取消、截止時間取消 context.WithDeadline、超時取消 context.WithTimeout 等。
比如有一個網絡請求 Request,每個 Request 都需要開啟一個 goroutine 做一些業務邏輯,這些 goroutine 又可能會開啟其他的 goroutine。那么這樣的話,我們就可以通過 Context 來跟蹤并控制這些 goroutine。
另外一個實際例子是,在 Go 實現的 web server 中,每個請求都會開一個 goroutine 去處理。但是我們的這個 goroutine 請求邏輯里面, 還需繼續創建goroutine 去訪問后端其他資源,比如數據庫、RPC 服務等。由于這些 goroutine 都是在處理同一個請求,因此,如果請求超時或者被取消后,所有的 goroutine 都應該馬上退出并且釋放相關的資源,這種情況也需要用 Context 來為我們取消掉所有 goroutine。
Context 的定義和實現
Context interface 接口定義
在 golang 里面,interface 是一個使用非常廣泛的結構,它可以接納任何類型。而 context 就是通過 interface 來定義的,定義很簡單,一共4個方法,這也是 Go 的設計理念,接口盡量簡單、小巧,通過組合來實現豐富的功能。
定義如下:
type Context interface { // 返回 context 是否會被取消以及自動取消的截止時間(即 deadline) Deadline() (deadline time.Time, ok bool) // 當 context 被取消或者到了 deadline,返回一個被關閉的 channel Done() <-chan struct{} // 返回取消的錯誤原因,因為什么 Context 被取消 Err() error // 獲取 key 對應的 value Value(key interface{}) interface{} }
- Deadline 返回 context 是否會被取消以及自動取消的截止時間,第一個返回值是截止時間,到了這個時間點,Context 會自動發起取消請求;第二個返回值 ok==false 時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。
- Done 方法返回一個只讀的 chan,類型為 struct{},如果該方法返回的 chan 可以讀取,那么就說明 parent context 已經發起了取消請求,當我們通過 Done 方法收到這個信號后,就應該做清理操作,然后退出 goroutine,釋放資源。之后,Err 方法會返回一個錯誤,告知為什么 Context 被取消。
- Err 方法返回取消的錯誤原因,因為什么 Context 被取消。
- Value 方法獲取該 Context 上保存的鍵值對,所以要通過一個 Key 才可以獲取對應的值,這個值一般是線程安全(并發安全)的。雖然 context 是一個并發安全的類型,但是如果 context 中保存著 value,則這些 value 通常不是并發安全的,并發讀寫這些 value 可能會造成數據錯亂,嚴重的情況下可能發生 panic,所以在并發時,如果我們的業務代碼需要讀寫 context 中的 value,那么最好建議我們 clone 一份原來的 context 中的 value,并塞到新的 ctx 傳遞給各個gorouinte。當然, 如果已經明確不會有并發讀取,那么可以直接使用,或者使用的時候加鎖。
parent Context 的具體實現
Context 雖然是個接口,但是并不需要使用方實現,golang 內置的 context 包,已經幫我們實現了,查看 Go 的源碼可以看到如下定義:
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
Background 和 TODO 兩個其實都是基于 emptyCtx 來實現的,emptyCtx 類型實現了 context 接口定義的 4 個方法,它本身是一個不可取消,沒有設置截止時間,沒有攜帶任何值的 Context,查看官方源碼如下:
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
Background 方法,一般是在 main 函數的入口處(或者請求最初的根 context)就定義并使用,然后一直往下傳遞,接下來所有的子協程里面都是基于 main 的 context 來衍生的。TODO 這個一般不建議業務上使用,一般沒有實際意義,在單元測試里面可以使用。
Context 的繼承和各種 With 系列函數
查看官方文檔 https://pkg.go.dev/golang.org/x/net/context
// 最基礎的實現,也可以叫做父 context func Background() Context func TODO() Context // 在 Background() 根 context 基礎上派生的各種 With 系列函數 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 interface{}, val interface{}) Context
- WithCancel 函數,傳遞一個 parent Context 作為參數,返回子 Context,以及一個取消函數用來取消 Context。我們前面說到控制父子協程的生命周期,那么就可以通過這個函數來實現
- WithDeadline 函數,和 WithCancel 差不多,但是它會多傳遞一個截止時間參數,這樣的話,當到了截止的時間點,就會自動取消 Context,當然我們也可以不等到這個時候,然后可以通過取消函數提前進行取消。
- WithTimeout 函數,和 WithDeadline 基本上一樣,會傳入一個 timeout 超時時間,也就是是從現在開始,直到過來 timeout 時間后,就進行超時取消,注意,這個是超時取消,不是截止時間取消。
- WithValue 函數,這個和 WithCancel 就沒有關系了,它不是用來控制父子協程生命周期的,這個是我們說到的,在 context 中傳遞基礎元數據用的,這個可以在 context 中存儲鍵值對的數據,然后這個鍵值對的數據可以通過 Context.Value 方法獲取到,這是我們實際用經常要用到的技巧,一般我們想要通過上下文來傳遞數據時,可以通過這個方法,如我們需要 tarceID 追蹤系統調用棧的時候。
Context 的常用方法實例
1. 調用 Context Done方法取消
func ContextDone(ctx context.Context, out chan<- Value) error { for { v, err := AllenHandler(ctx) if err != nil { return err } select { case <-ctx.Done(): log.Infof("context has done") return ctx.Err() case out <- v: } } }
2. 通過 context.WithValue 來傳值
func main() { ctx, cancel := context.WithCancel(context.Background()) valueCtx := context.WithValue(ctx, key, "add value from allen") go watchAndGetValue(valueCtx) time.Sleep(10 * time.Second) cancel() time.Sleep(5 * time.Second) } func watchAndGetValue(ctx context.Context) { for { select { case <-ctx.Done(): //get value log.Infof(ctx.Value(key), "is cancel") return default: //get value log.Infof(ctx.Value(key), "int goroutine") time.Sleep(2 * time.Second) } } }
3. 超時取消 context.WithTimeout
package main import ( "fmt" "sync" "time" "golang.org/x/net/context" ) var ( wg sync.WaitGroup ) func work(ctx context.Context) error { defer wg.Done() for i := 0; i < 1000; i++ { select { case <-time.After(2 * time.Second): fmt.Println("Doing some work ", i) // we received the signal of cancelation in this channel case <-ctx.Done(): fmt.Println("Cancel the context ", i) return ctx.Err() } } return nil } func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() fmt.Println("Hey, I'm going to do some work") wg.Add(1) go work(ctx) wg.Wait() fmt.Println("Finished. I'm going home") }
4. 截止時間取消 context.WithDeadline
package main import ( "context" "fmt" "time" ) func main() { d := time.Now().Add(1 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), d) // Even though ctx will be expired, it is good practice to call its // cancelation function in any case. Failure to do so may keep the // context and its parent alive longer than necessary. defer cancel() select { case <-time.After(2 * time.Second): fmt.Println("oversleep") case <-ctx.Done(): fmt.Println(ctx.Err()) } }
Context 使用原則 和 技巧
- Context 是線程安全的,可以放心的在多個 goroutine 協程中傳遞
- 可以把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操作時,所有 goroutine 都會接收到取消信號。
- 不要把 Context 放在結構體中,要以參數的方式傳遞,parent Context 一般為Background,并且一般要在 main 函數的入口處創建然后傳遞下去
- Context 的變量名建議都統一為 ctx,并且要把 Context 作為第一個參數傳遞給入口請求和出口請求鏈路上的每一個函數
- 往下游給一個函數方法傳遞 Context 的時候,千萬不要傳遞 nil,否則在 tarce 追蹤的時候,就會中斷鏈路,并且如果函數里面有獲取值的邏輯,可能導致 panic。
- Context 的 Value 只能傳遞一些通用或者基礎的元數據,不要傳遞業務層面的數據,不是說不可以傳遞,是在 Go 的編碼規范或者慣用法中不提倡不要什么數據都使用這個傳遞。由于 context 存儲 key-value 是鏈式的,因此查詢復雜度為O(n),所以,盡量不要隨意存儲不必要的數據
原文鏈接:https://juejin.cn/post/7171022350303887397
相關推薦
- 2023-10-09 grid網格布局
- 2021-12-11 C語言中數據在內存如何存儲_C 語言
- 2022-09-16 Pandas索引排序?df.sort_index()的實現_python
- 2021-12-01 C語言中單目操作符++、–的實例講解_C 語言
- 2022-05-18 python操作jira添加模塊的方法_python
- 2022-05-03 Python?PyQt5學習之自定義信號_python
- 2022-07-28 docker容器間進行數據共享的三種實現方式_docker
- 2022-07-15 C語言函數棧幀的創建與銷毀原理圖解_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支