一、起源
context包最初由Go語言的核心團隊提出,并于Go 1.7版本正式引入。其設計靈感來源于Google內部的實踐,旨在解決長期運行的并發任務和服務之間如何協調取消信號和超時控制的問題。在早期的Go語言編程中,開發者通常使用全局變量或者依賴參數傳遞來實現上下文管理,這種方式在多層嵌套和并發執行時,容易出現代碼可讀性差、維護困難等問題。因此,Go語言的設計者決定創建一個標準化的解決方案,context應運而生。
二、主要作用
context的主要作用是在并發操作中傳遞控制信號,特別是在涉及多個goroutine的操作時,它提供了以下幾個核心功能:
-
取消信號(Cancellation):通過
context.CancelFunc,Go語言允許父任務通知其子任務取消操作,這對于清理資源、停止不再需要的任務、避免內存泄漏等至關重要。 -
超時控制(Timeout/Deadline):通過
context.WithTimeout或context.WithDeadline,開發者可以設置任務的最大執行時間,當超時到達時,context會自動觸發取消信號,幫助避免長時間掛起的操作。 -
跨goroutine的共享數據(Request-scoped Data):
context允許在多個goroutine之間傳遞元數據,特別是在一個請求生命周期內共享的值,這對于記錄日志、追蹤請求ID或傳遞認證信息等非常有用。 -
避免資源泄漏:通過控制超時和取消操作,
context有助于及時清理資源,避免因未取消的操作或長期掛起的goroutine造成系統負擔。
三、使用方式
1. 引入 context 包
在 Go 中使用 context,首先需要引入 context 包:
import "context"
2. 創建一個 context
context 的創建通常通過 context.Background() 或 context.TODO() 方法來完成。
context.Background():一般用作主程序的頂層context,通常在主函數中使用。context.TODO():當你不確定該使用什么context時,可以使用context.TODO()。通常用于暫時的占位符。
示例:
ctx := context.Background()
3. 創建具有取消功能的 context
context 包提供了 WithCancel、WithTimeout 和 WithDeadline 等函數來創建具有特定功能的 context。
3.1 使用 context.WithCancel
context.WithCancel 用于創建一個可以手動取消的 context。它返回一個子 context 和一個取消函數 CancelFunc,你可以調用取消函數來取消該 context,從而通知相關的 goroutine 停止執行。
示例:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 確保在函數結束時取消
// 假設在 goroutine 中運行的任務
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任務完成")
case <-ctx.Done():
fmt.Println("任務被取消:", ctx.Err())
}
}(ctx)
// 取消操作
time.Sleep(2 * time.Second) // 模擬一段時間后取消
cancel()
3.2 使用 context.WithTimeout
context.WithTimeout 用于設置一個超時期限,當超時到達時,會自動取消該 context。
示例:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 確保在函數結束時取消
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任務完成")
case <-ctx.Done():
fmt.Println("任務超時:", ctx.Err())
}
}(ctx)
在這個例子中,任務將在 2 秒后被取消,因為超時設置為 2 秒。
3.3 使用 context.WithDeadline
context.WithDeadline 用于創建一個具有特定截止時間的 context。當達到截止時間時,context 會自動被取消。
示例:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel() // 確保在函數結束時取消
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任務完成")
case <-ctx.Done():
fmt.Println("任務到期:", ctx.Err())
}
}(ctx)
4. 傳遞 context 給子 goroutine
通常,context 會在父 goroutine 中創建,并傳遞給子 goroutine。這樣,子 goroutine 可以根據父 goroutine 傳遞的 context 來判斷任務是否被取消、是否超時,或者共享一些數據。
示例:
func doTask(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("任務完成")
case <-ctx.Done():
fmt.Println("任務被取消:", ctx.Err())
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go doTask(ctx)
time.Sleep(1 * time.Second) // 等待一段時間
cancel() // 取消任務
}
5. 在 context 中傳遞數據
context 允許在多個 goroutine 之間傳遞請求相關的數據(例如,請求 ID 或認證信息)。可以通過 context.WithValue 創建一個新的 context,并將鍵值對數據存儲在 context 中。
注意:不要用 context 傳遞大數據或敏感信息,因為它的主要目的是攜帶跨 API 邊界和跨 goroutine 的請求級別的數據。
示例:
type key string
const UserIDKey key = "userID"
func doTask(ctx context.Context) {
userID := ctx.Value(UserIDKey)
fmt.Println("UserID:", userID)
}
func main() {
ctx := context.Background()
// 將數據傳遞給上下文
ctx = context.WithValue(ctx, UserIDKey, 12345)
go doTask(ctx)
time.Sleep(1 * time.Second) // 等待goroutine執行
}
6. context 的取消與清理
每當使用 context.WithCancel、context.WithTimeout 或 context.WithDeadline 時,記得使用 defer cancel() 來確保在不再需要該 context 時進行清理。這會幫助避免泄漏和過度消耗資源。
7. context 的嵌套與傳遞
context 是可以嵌套的。當你通過 WithCancel、WithTimeout 等函數創建子 context 時,子 context 會繼承父 context 的值和狀態。如果父 context 被取消或超時,子 context 也會被取消。
示例:
func main() {
ctx := context.Background()
// 創建一個具有 3 秒超時的子 context
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
// 創建另一個子 context,并在其中傳遞數據
ctx = context.WithValue(ctx, "userID", 42)
go func(ctx context.Context) {
select {
case <-time.After(5 * time.Second):
fmt.Println("任務完成")
case <-ctx.Done():
fmt.Println("任務被取消:", ctx.Err())
userID := ctx.Value("userID")
fmt.Println("用戶 ID:", userID)
}
}(ctx)
time.Sleep(4 * time.Second)
}
在上面的代碼中,子context會繼承父context的取消機制,并且傳遞了用戶 ID 的信息。