前言
Context 是 Go 1.7 添加於標準函式庫的功能。常在存取資料庫或其他服務時會遇到,初步看起來是用於「傳遞取消信號」用途的語法,用於處理 goroutine:
- 背負期限(deadline)
- 取消信號(cancellation signal)
- 傳遞請求相關的值(request-scoped values)
Context 是啥?
舉例當一個請求會衍生出多個 goroutine、或會呼叫多層函式時,常會遇到幾個問題:
- 上層請求已經結束了,但底下 goroutine 還在跑
- 某個操作卡住,希望超時後能全部中斷
- 想在整條呼叫鏈中攜帶
request id之類的資訊
Go context 包了 channel 在 goroutine 間傳遞,定義其存在的理由(某條件或期限達成),而這個理由應該與 context 相關聯。
// Context 背負期限(deadline)、取消信號(cancellation signal)、與請求相關的值(request-scoped values)// 它的方法可以被多個 goroutine 同時安全地使用。type Context interface { // Done 會回傳一個 channel, // 當此 Context 被取消或逾時(timeout)時,該 channel 會被關閉。 Done() <-chan struct{}
// Err 會在 Done channel 關閉之後, // 回傳此 Context 被取消的原因。 Err() error
// Deadline 會回傳此 Context 預計被取消的時間(如果有設定的話)。 // ok 表示是否存在 deadline。 Deadline() (deadline time.Time, ok bool)
// Value 會回傳與指定 key 關聯的值; // 如果沒有對應的值,則回傳 nil。 Value(key interface{}) interface{}}Done() <-chan struct{} 用例
結束時 Done() channel 才會被關閉。未關閉前會返回 nil。
- Context 被取消(cancel)
- 超時(timeout)
- parent Context 結束
select {case <-ctx.Done(): // 收到取消或超時訊號 return ctx.Err()case result := <-workChan: return result}Err() error 用例
當 Context 結束後得知原因。未關閉前會返回 nil。
<-ctx.Done()log.Println(ctx.Err())Deadline() (time.Time, bool) 用例
查 Context 是否有設定截止時間。
ok == false:沒有 deadlineok == true:deadline 是什麼時候
Value(key interface{}) interface{} 用例
Value 允許你在 Context 裡存放「請求範圍內」的資料,例如:
// 上游創建 contextctx = context.WithValue(ctx, "request_id", "abc-123")
// 下游讀取 contextreqID := ctx.Value("request_id")與 Context 搭配的方法
Background
所有 Context 的根節點,通常用於主函式、初始化或測試中
返回一個非 nil 的空 Context,它:
- 永遠不會被取消
- 沒有截止時間
- 沒有攜帶任何值
// ctx1 是根節點(Background)ctx1 := context.Background()
// ctx2 是 ctx1 的子節點ctx2 := context.WithValue(ctx1, "key", "value") // 仍然不可取消(只是帶了值)
// ctx3 是 ctx2 的子節點ctx3, cancel := context.WithTimeout(ctx2, 5*time.Second) // 可以取消(有超時)defer cancel()context.Background() (永遠不會被取消) │ └─> WithValue(...) (繼承父節點,仍不可取消,但帶了值) │ └─> WithTimeout(...) (可以被取消!5秒後自動取消) │ └─> WithCancel(...) (可以被取消!呼叫 cancel() 時取消)TODO
當你不確定該用哪個 Context 時的佔位符
func someFunction() { // 暫時不確定要用什麼 Context,先用 TODO ctx := context.TODO() anotherFunction(ctx)}WithCancel
當你希望「某個事件發生時,主動中止所有相關 goroutine」
ctx, cancel := context.WithCancel(context.Background())defer cancel() // 很重要,避免資源洩漏
go func() { time.Sleep(2 * time.Second) cancel()}()
<-ctx.Done()fmt.Println(ctx.Err()) // context.CanceledWithDeadline
當你希望「在指定的時間點之前完成,主動中止所有相關 goroutine」
// API 必須在特定時間前回應deadline := time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)ctx, cancel := context.WithDeadline(ctx, deadline)defer cancel()
resp, err := client.DoWithContext(ctx, req)WithTimeout
當你希望「某個期限過後,主動中止所有相關 goroutine」
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()
select {case <-time.After(5 * time.Second): fmt.Println("finished work")case <-ctx.Done(): fmt.Println(ctx.Err()) // context.DeadlineExceeded}WithValue
Context 數值應基於 request 範圍內的資料傳輸過程和 API 數據
// 上游創建 contextctx := context.Background()ctx = context.WithValue(ctx, "request_id", "abc-123")
// 下游讀取 contextreqID := ctx.Value("request_id")// 創建私有型別避免 key 撞名type ctxKeyRequestID struct{}ctx = context.WithValue(ctx, ctxKeyRequestID{}, "abc-123")總結
Go Context 是一種官方方式去管理 Go Routine 的生命週期基於 Channel
目前我只在與 DB 互動上使用到這項功能,或許之後會遇到更多情境幫得上忙。就是一種官方的方式管理 Go Routine 的生命週期。
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()
var todos []models.Todocursor, err := db.GetCollection().Find(ctx, bson.M{})延伸閱讀
- Learning Golang Context!! Never Looked At It! - TheVimeagen
- Go Concurrency Patterns: Context - Go
- Go From Zero to Depth — Part 8: Context Isn’t Cancellation — It’s a Protocol