之前有兄弟留言想學習一下 Context
,他來了,雖遲但到。
前言
在 Go 語言中,Context 是一個非常重要的概念,它用於在不同的 goroutine 之間傳遞請求域的相關資料,並且可以用來控制 goroutine 的生命週期和取消操作。本文將深入探討 Go 語言中 Context 特性 和 Context 的高階使用方法。
基本用法
在 Go 語言中,Context 被定義為一個介面型別,它包含了三個方法:
# go version 1.18.10
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline() 方法用於獲取 Context 的截止時間,
- Done() 方法用於返回一個只讀的 channel,用於通知當前 Context 是否已經被取消,
- Err() 方法用於獲取 Context 取消的原因,
- Value() 方法用於獲取 Context 中儲存的鍵值對資料。
我們日常編寫程式碼時,Context 物件會被被約定作為函式的第一個引數傳遞,eg:
func users(ctx context.Context, request *Request) {
// ... code
}
在函式中,可以透過 ctx 引數來獲取相關的 Context 資料,舉個超時的 eg:
deadline, ok := ctx.Deadline()
if ok && deadline.Before(time.Now()) {
// 超時
return
}
Context 控制 goroutine 的生命週期
在 Go 語言中,goroutine 是一種非常常見的併發程式設計模型,而 Context 可以被用來控制 goroutine 的生命週期,從而避免出現 goroutine 洩漏或者不必要的等待操作。
eg,看一下下方程式碼:
func users(ctx context.Context, req *Request) {
// 啟動一個 goroutine 來處理請求
go func() {
// 處理請求...
}()
}
上面的程式碼中,我們啟動了一個 goroutine 來處理請求,但是沒有任何方式來控制這個 goroutine 的生命週期,如果這個請求被取消了,那麼這個 goroutine 就會一直存在,直到它完成為止。為了避免這種情況的發生,我們可以使用 Context 來控制 goroutine 的生命週期,eg:
func users(ctx context.Context, req *Request) {
// 啟動一個 goroutine 來處理請求
go func(ctx context.Context) {
// 處理請求...
}(ctx)
}
在上面的程式碼中,我們將 Context 物件作為引數傳遞給了 goroutine 函式,這樣在請求被取消時,goroutine 就可以及時退出。
除了用於控制 goroutine 的生命週期,Context 還可以被用來在不同的 goroutine 之間傳遞請求域的相關資料。為了實現這個目的,我們可以使用 Context 的 WithValue() 方法,eg:
type key int
const (
userKey key = iota
)
func users(ctx context.Context, req *Request) {
// 從請求中獲取使用者資訊
user := req.GetUser
// 將使用者資訊儲存到 Context 中
ctx = context.WithValue(ctx, userKey, user)
// 啟動一個 goroutine 來處理請求
go func(ctx context.Context) {
// 從 Context 中獲取使用者資訊
user := ctx.Value(userKey).(*User)
// 處理請求...
}(ctx)
}
在上面的程式碼中,我們定義了一個 key
型別的常量 userKey
,然後在 users()
函式中將使用者資訊儲存到了 Context 中,並將 Context 物件傳遞給了 goroutine 函式。
在 goroutine 函式中,我們使用 ctx.Value()
方法來獲取 Context 中儲存的使用者資訊。
注:
Context 中儲存的鍵值對資料應該是執行緒安全的,因為它們可能會在多個 goroutine 中同時訪問。
使用 WithCancel() 取消操作
除了控制 goroutine 的生命週期和傳遞資料之外,Context 還可以被用來執行取消操作。為了實現這個目的,我們可以使用 Context 的 WithCancel()
方法,eg:
func users(ctx context.Context, req *Request) {
// 建立一個可以取消的 Context 物件
ctx, cancel := context.WithCancel(ctx)
// 啟動一個 goroutine 來處理請求
go func(ctx context.Context) {
// 等待請求完成或者被取消
select {
case <-time.After(time.Second):
// 請求完成
fmt.Println("Request finish")
case <-ctx.Done():
// 請求被取消
fmt.Println("Request canceled")
}
}(ctx)
// 等待一段時間後取消請求
time.Sleep(time.Millisecond * 800)
cancel()
}
在上面的程式碼中,我們使用 WithCancel() 方法建立了一個可以取消的 Context 物件,並將取消操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者被取消,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。
使用 WithDeadline() 設定截止時間
除了使用 WithCancel() 方法進行取消操作之外,Context 還可以被用來設定截止時間,以便在超時的情況下取消請求。為了實現這個目的,我們可以使用 Context 的 WithDeadline() 方法,eg:
func users(ctx context.Context, req *Request) {
// 設定請求的截止時間為當前時間加上 1 秒鐘
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second))
// 啟動一個 goroutine 來處理請求
go func(ctx context.Context) {
// 等待請求完成或者超時
select {
case <-time.After(time.Millisecond * 500):
// 請求完成
fmt.Println("Request finish")
case <-ctx.Done():
// 請求超時或者被取消
fmt.Println("Request canceled or timed out")
}
}(ctx)
// 等待一段時間後取消請求
time.Sleep(time.Millisecond * 1500)
cancel()
}
在上面的程式碼中,我們使用 WithDeadline() 方法設定了一個截止時間為當前時間加上 1 秒鐘的 Context 物件,並將超時操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者超時,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。
注:
在使用 WithDeadline() 方法設定截止時間的時候,如果截止時間已經過期,則 Context 物件將被立即取消。
除了使用 WithDeadline() 方法進行截止時間設定之外,Context 還可以被用來設定超時時間。為了實現這個目的,我們可以使用 Context 的 WithTimeout() 方法,eg:
func users(ctx context.Context, req *Request) {
// 設定請求的超時時間為 1 秒鐘
ctx, cancel := context.WithTimeout(ctx, time.Second)
// 啟動一個 goroutine 來處理請求
go func(ctx context.Context) {
// 等待請求完成或者超時
select {
case <-time.After(time.Millisecond * 500):
// 請求完成
fmt.Println("Request completed")
case <-ctx.Done():
// 請求超時或者被取消
fmt.Println("Request canceled or timed out")
}
}(ctx)
// 等待一段時間後取消請求
time.Sleep(time.Millisecond * 1500)
cancel()
}
在上面的程式碼中,我們使用 WithTimeout() 方法設定了一個超時時間為 1 秒鐘的 Context 物件,並將超時操作封裝在了一個 cancel() 函式中。然後我們啟動了一個 goroutine 函式,使用 select 語句等待請求完成或者超時,最後在主函式中等待一段時間後呼叫 cancel() 函式來取消請求。
注:
需要注意的是,在使用 WithTimeout() 方法設定超時時間的時候,如果超時時間已經過期,則 Context 物件將被立即取消。
在一個應用程式中,不同的 goroutine 可能需要共享同一個 Context 物件。為了實現這個目的,Context 物件可以透過函式呼叫或者網路傳輸等方式進行傳遞。
例如,我們可以在一個處理 HTTP 請求的函式中建立一個 Context 物件,並將它作為引數傳遞給一個資料庫查詢函式,以便在查詢過程中使用這個 Context 物件進行取消操作。程式碼 eg:
func users(ctx context.Context, req *Request) {
// 在處理 HTTP 請求的函式中建立 Context 物件
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// 呼叫資料庫查詢函式並傳遞 Context 物件
result, err := findUserByName(ctx, req)
if err != nil {
// 處理查詢錯誤...
}
// 處理查詢結果...
}
func findUserByName(ctx context.Context, req *Request) (*Result, error) {
// 在資料庫查詢函式中使用傳遞的 Context 物件
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE name = ?", req.Name)
if err != nil {
// 處理查詢錯誤...
}
defer rows.Close()
// 處理查詢結果...
}
在上面的程式碼中,我們在處理 HTTP 請求的函式中建立了一個 Context 物件,並將它作為引數傳遞給了一個資料庫查詢函式 findUserByName()
。在 findUserByName()
函式中,我們使用傳遞的 Context 物件來呼叫 db.QueryContext()
方法進行查詢操作。由於傳遞的 Context 物件可能會在查詢過程中被取消,因此我們需要在查詢完成後檢查查詢操作的錯誤,以便進行相應的處理。
注:
在進行 Context 的傳遞時,我們需要保證傳遞的 Context 物件是原始 Context 物件的子 Context,以便在需要取消操作時能夠同時取消所有相關的 goroutine。如果傳遞的 Context 物件不是原始 Context 物件的子 Context,則取消操作只會影響到當前 goroutine,而無法取消其他相關的 goroutine。
在 Go 語言中,Context 是一個非常重要的特性,包括其基本用法和一些高階用法。Context 可以用於管理 goroutine 的生命週期和取消操作,避免出現資源洩漏和死鎖等問題,同時也可以提高應用程式的效能和可維護性。
在使用 Context 的時候,需要注意以下幾點:
- 在建立 goroutine 時,需要將原始 Context 物件作為引數傳遞給它。
- 在 goroutine 中,需要使用傳遞的 Context 物件來進行取消操作,以便能夠及時釋放相關的資源。
- Context 的傳遞時,需要保證傳遞的 Context 物件是原始 Context 物件的子 Context,以便在需要取消操作時能夠同時取消所有相關的 goroutine。
- 在使用 WithCancel 和 WithTimeout 方法建立 Context 物件時,需要及時呼叫 cancel 函式,以便能夠及時釋放資源。
- 在一些場景下,可以使用 WithValue 方法將資料儲存到 Context 中,以便在不同的 goroutine 之間共享資料。
本作品採用《CC 協議》,轉載必須註明作者和本文連結