當(dāng)需要在多個 goroutine 中傳遞上下文信息時,可以使用 Context 實現(xiàn)。Context 除了用來傳遞上下文信息,還可以用于傳遞終結(jié)執(zhí)行子任務(wù)的相關(guān)信號,中止多個執(zhí)行子任務(wù)的 goroutine。context 中提供以下接口:
(context包下的接口)
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline 方法,返回 Context 被取消的時間,也就是完成工作的截止日期;
Done,返回一個 channel,這個 channel 會在當(dāng)前工作完成或者上下文被取消之后關(guān)閉,多次調(diào)用 Done 方法會返回同一個 channel;
Err 方法,返回 Context 結(jié)束的原因,它只會在 Done 返回的 channel 被關(guān)閉時才會返回非空的值,如果 Context 被取消,會返回 Canceled 錯誤;如果 Context 超時,會返回 DeadlineExceeded 錯誤。
Value 方法,可用于從 Context 中獲取傳遞的鍵值信息。
在 Web 請求的處理過程中,一個請求可能啟動多個 goroutine 協(xié)同工作,這些 goroutine 之間可能需要共享請求的信息,且當(dāng)請求被取消或者執(zhí)行超時時,該請求對應(yīng)的所有 goroutine 都需要快速結(jié)束,釋放資源。Context 就是為了解決上述場景而開發(fā)的,我們通過下面一個例子來演示:
package main
import (
"context"
"fmt"
"time"
)
const DB_ADDRESS = "db_address"
const CALCULATE_VALUE = "calculate_value"
func readDB(ctx context.Context, cost time.Duration) {
fmt.Println("db address is", ctx.Value(DB_ADDRESS))
select {
case <- time.After(cost): // 模擬數(shù)據(jù)庫讀取
fmt.Println("read data from db")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任務(wù)取消的原因
// 一些清理工作
}
}
func calculate(ctx context.Context, cost time.Duration) {
fmt.Println("calculate value is", ctx.Value(CALCULATE_VALUE))
select {
case <- time.After(cost): // 模擬數(shù)據(jù)計算
fmt.Println("calculate finish")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 任務(wù)取消的原因
// 一些清理工作 比如關(guān)閉goroutine
}
}
func main() {
ctx := context.Background(); // 創(chuàng)建一個空的上下文
// 添加上下文信息
ctx = context.WithValue(ctx, DB_ADDRESS, "localhost:10086")
ctx = context.WithValue(ctx, CALCULATE_VALUE, 1234)
// 設(shè)定子 Context 2s 后執(zhí)行超時返回
ctx, cancel := context.WithTimeout(ctx, time.Second*2) //會返回一個取消函數(shù)
defer cancel()
// 設(shè)定執(zhí)行時間為 4 s
go readDB(ctx, time.Second*4)
go calculate(ctx, time.Second*4)
// 充分執(zhí)行
time.Sleep(time.Second * 5)
}
用context作為協(xié)成的參數(shù)
在上述例子中,我們模擬了一個請求中同時進(jìn)行數(shù)據(jù)庫訪問和邏輯計算的操作,在請求執(zhí)行超時時,及時關(guān)閉尚未執(zhí)行結(jié)束 goroutine。我們首先通過 context.WithValue 方法為 context 添加上下文信息,Context 在多個 goroutine 中是并發(fā)安全的,可以安全地在多個 goroutine 中對 Context 中的上下文數(shù)據(jù)進(jìn)行讀取。接著使用 context.WithTimeout 方法設(shè)定了 Context 的超時時間為 2s,并傳遞給 readDB 和 calculate 兩個 goroutine 執(zhí)行子任務(wù)。在 readDB 和 calculate 方法中,使用 select 語句對 Context 的 Done 通道進(jìn)行監(jiān)控。由于我們設(shè)定了子 Context 將在 2s 之后超時,所以它將在 2s 之后關(guān)閉 Done 通道;然而預(yù)設(shè)的子任務(wù)執(zhí)行時間為 4s,對應(yīng)的 case 語句尚未返回,執(zhí)行被取消,進(jìn)入到清理工作的 case 語句中,結(jié)束掉當(dāng)前的 goroutine 所執(zhí)行的任務(wù)。預(yù)期的輸出結(jié)果如下:

這就是超時報錯
如果你想看看取消不錯,可以修改主goroutine的slesp為1秒,然后再defer語句上添加defer fmt.Println(ctx.Err())
使用 Context,能夠有效地在一組 goroutine 中傳遞共享值、取消信號、deadline 等信息,及時關(guān)閉不需要的 goroutine。