Go語言高級特性:Context深入解讀
概述
在 Go 語言中,context(上下文)是一個非常重要的概念。
它主要用于在多個 goroutine 之間傳遞請求特定任務(wù)的截止日期、取消信號以及其他請求范圍的值。3. Context 的取消與超時
本文將探討 Go 語言中context的用法,從基礎(chǔ)概念到實際應(yīng)用,將全面了解上下文的使用方法。
主要內(nèi)容包括
什么是 Context(上下文)
Context 的基本用法:創(chuàng)建與傳遞
Context 的取消與超時
Context 的值傳遞
實際應(yīng)用場景:HTTP 請求的 Context 使用
數(shù)據(jù)庫操作中的 Context 應(yīng)用
自定義 Context 的實現(xiàn)
Context 的生命周期管理
Context 的注意事項
1. 什么是 Context(上下文)
在 Go 語言中,context(上下文)是一個接口,定義了四個方法:Deadline()、Done()、Err()和Value(key interface{})。
它主要用于在 goroutine 之間傳遞請求的截止日期、取消信號以及其他請求范圍的值。
2. Context 的基本用法:創(chuàng)建與傳遞
2.1 創(chuàng)建 Context
package main
import (
"context"
"fmt"
)
func main() {
// 創(chuàng)建一個空的Context
ctx := context.Background()
// 創(chuàng)建一個帶有取消信號的Context
_, cancel := context.WithCancel(ctx)
defer cancel() // 延遲調(diào)用cancel,確保在函數(shù)退出時取消Context
fmt.Println("Context創(chuàng)建成功")
}以上代碼演示了如何創(chuàng)建一個空的context和一個帶有取消信號的context。
使用context.WithCancel(parent)可以創(chuàng)建一個帶有取消信號的子context。
在調(diào)用cancel函數(shù)時,所有基于該context的操作都會收到取消信號。
2.2 傳遞 Context
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信號,任務(wù)結(jié)束")
return
default:
fmt.Println("正在執(zhí)行任務(wù)...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx := context.Background()
ctxWithCancel, cancel := context.WithCancel(ctx)
defer cancel()
go worker(ctxWithCancel)
time.Sleep(3 * time.Second)
cancel() // 發(fā)送取消信號
time.Sleep(1 * time.Second)
}在上面例子中,創(chuàng)建了一個帶有取消信號的context,并將它傳遞給一個 goroutine 中的任務(wù)。
調(diào)用cancel函數(shù),可以發(fā)送取消信號,中斷任務(wù)的執(zhí)行。
3.Context 的取消與超時
3.1 使用 Context 實現(xiàn)取消
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctxWithCancel, cancel := context.WithCancel(ctx)
go func() {
select {
case <-ctxWithCancel.Done():
fmt.Println("收到取消信號,任務(wù)結(jié)束")
case <-time.After(2 * time.Second):
fmt.Println("任務(wù)完成")
}
}()
time.Sleep(1 * time.Second)
cancel() // 發(fā)送取消信號
time.Sleep(1 * time.Second)
}在這個例子中,使用time.After函數(shù)模擬一個長時間運行的任務(wù)。
如果任務(wù)在 2 秒內(nèi)完成,就打印“任務(wù)完成”,否則在接收到取消信號后打印“收到取消信號,任務(wù)結(jié)束”。
3.2 使用 Context 實現(xiàn)超時控制
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
select {
case <-ctxWithTimeout.Done():
fmt.Println("超時,任務(wù)結(jié)束")
case <-time.After(1 * time.Second):
fmt.Println("任務(wù)完成")
}
}在上面例子中,使用context.WithTimeout(parent, duration)函數(shù)創(chuàng)建了一個在 2 秒后自動取消的context
如果任務(wù)在 1 秒內(nèi)完成,就打印“任務(wù)完成”,否則在 2 秒后打印“超時,任務(wù)結(jié)束”。
4. Context 的值傳遞
4.1 使用 WithValue 傳遞值
package main
import (
"context"
"fmt"
)
type key string
func main() {
ctx := context.WithValue(context.Background(), key("name"), "Alice")
value := ctx.Value(key("name"))
fmt.Println("Name:", value)
}在上面例子中,使用context.WithValue(parent, key, value)函數(shù)在context中傳遞了一個鍵值對。
使用ctx.Value(key)函數(shù)可以獲取傳遞的值。
5. 實際應(yīng)用場景:HTTP 請求的 Context 使用
5.1 使用 Context 處理 HTTP 請求
package main
import (
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(3 * time.Second):
fmt.Fprintln(w, "Hello, World!")
case <-ctx.Done():
err := ctx.Err()
fmt.Println("處理請求:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}在上面例子中,使用http.Request結(jié)構(gòu)體中的Context()方法獲取到請求的context,并在處理請求時設(shè)置了 3 秒的超時時間。
如果請求在 3 秒內(nèi)完成,就返回“Hello, World!”,否則返回一個錯誤。
5.2 處理 HTTP 請求的超時與取消
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Fprintln(w, "Hello, World!")
case <-ctx.Done():
err := ctx.Err()
fmt.Println("處理請求:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}在上面例子中,使用context.WithTimeout(parent, duration)函數(shù)為每個請求設(shè)置了 2 秒的超時時間。
如果請求在 2 秒內(nèi)完成,就返回“Hello, World!”,否則返回一個錯誤。
6. 數(shù)據(jù)庫操作中的 Context 應(yīng)用
6.1 使用 Context 控制數(shù)據(jù)庫查詢的超時
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/database")
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
fmt.Println("查詢出錯:", err)
return
}
defer rows.Close()
// 處理查詢結(jié)果
}在上面例子中,使用context.WithTimeout(parent, duration)函數(shù)為數(shù)據(jù)庫查詢設(shè)置了 2 秒的超時時間,確保在 2 秒內(nèi)完成查詢操作。
如果查詢超時,程序會收到context的取消信號,從而中斷數(shù)據(jù)庫查詢。
6.2 使用 Context 取消長時間運行的數(shù)據(jù)庫事務(wù)
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "username:password@tcp(localhost:3306)/database")
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
fmt.Println(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go func() {
<-ctx.Done()
fmt.Println("接收到取消信號,回滾事務(wù)")
tx.Rollback()
}()
// 執(zhí)行長時間運行的事務(wù)操作
// ...
err = tx.Commit()
if err != nil {
fmt.Println("提交事務(wù)出錯:", err)
return
}
fmt.Println("事務(wù)提交成功")
}在這個例子中,使用context.WithTimeout(parent, duration)函數(shù)為數(shù)據(jù)庫事務(wù)設(shè)置了 2 秒的超時時間。
同時使用 goroutine 監(jiān)聽context的取消信號。
在 2 秒內(nèi)事務(wù)沒有完成,程序會收到context的取消信號,從而回滾事務(wù)。
7. 自定義 Context 的實現(xiàn)
7.1 實現(xiàn)自定義的 Context 類型
package main
import (
"context"
"fmt"
"time"
)
type MyContext struct {
context.Context
RequestID string
}
func WithRequestID(ctx context.Context, requestID string) *MyContext {
return &MyContext{
Context: ctx,
RequestID: requestID,
}
}
func (c *MyContext) Deadline() (deadline time.Time, ok bool) {
return c.Context.Deadline()
}
func (c *MyContext) Done() <-chan struct{} {
return c.Context.Done()
}
func (c *MyContext) Err() error {
return c.Context.Err()
}
func (c *MyContext) Value(key interface{}) interface{} {
if key == "requestID" {
return c.RequestID
}
return c.Context.Value(key)
}
func main() {
ctx := context.Background()
ctxWithRequestID := WithRequestID(ctx, "12345")
requestID := ctxWithRequestID.Value("requestID").(string)
fmt.Println("Request ID:", requestID)
}在這個例子中,實現(xiàn)了一個自定義的MyContext類型,它包含了一個RequestID字段。
通過WithRequestID函數(shù),可以在原有的context上附加一個RequestID值,然后在需要的時候獲取這個值。
7.2 自定義 Context 的應(yīng)用場景
自定義context類型的應(yīng)用場景非常廣泛,比如在微服務(wù)架構(gòu)中,
可在context中加入一些額外的信息,比如用戶 ID、請求來源等,以便在服務(wù)調(diào)用鏈路中傳遞這些信息。
8. Context 的生命周期管理
8.1 Context 的生命周期
context.Context 是不可變的,一旦創(chuàng)建就不能修改。它的值在傳遞時是安全的,可以被多個 goroutine 共享。
在一個請求處理的生命周期內(nèi),通常會創(chuàng)建一個頂級的 Context,然后從這個頂級的 Context 派生出子 Context,傳遞給具體的處理函數(shù)。
8.2 Context 的取消
當(dāng)請求處理完成或者發(fā)生錯誤時,應(yīng)該主動調(diào)用 context.WithCancel 或 context.WithTimeout 等函數(shù)創(chuàng)建的 Context 的 Cancel 函數(shù)來通知子 goroutines 停止工作。
這樣可以確保資源被及時釋放,避免 goroutine 泄漏。
func handleRequest(ctx context.Context) {
// 派生一個新的 Context,設(shè)置超時時間
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel() // 確保在函數(shù)退出時調(diào)用 cancel,防止資源泄漏
// 在這個新的 Context 下進行操作
// ...
}8.3 Context 的傳遞
在函數(shù)之間傳遞 Context 時,確保傳遞的是必要的最小信息。避免在函數(shù)間傳遞整個 Context,而是選擇傳遞 Context 的衍生物。
如 context.WithValue 創(chuàng)建的 Context,其中包含了特定的鍵值對信息。
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 從請求中獲取特定的信息
userID := extractUserID(r)
// 將特定的信息存儲到 Context 中
ctx := context.WithValue(r.Context(), userIDKey, userID)
// 將新的 Context 傳遞給下一個處理器
next.ServeHTTP(w, r.WithContext(ctx))
})
}9. Context 的注意事項
9.1 不要在函數(shù)簽名中傳遞 Context
避免在函數(shù)的參數(shù)列表中傳遞 context.Context,除非確實需要用到它。
如果函數(shù)的邏輯只需要使用 Context 的部分功能,那么最好只傳遞它需要的具體信息,而不是整個 Context。
// 不推薦的做法
func processRequest(ctx context.Context) {
// ...
}
// 推薦的做法
func processRequest(userID int, timeout time.Duration) {
// 使用 userID 和 timeout,而不是整個 Context
}9.2 避免在結(jié)構(gòu)體中濫用 Context
不要在結(jié)構(gòu)體中保存 context.Context,因為它會增加結(jié)構(gòu)體的復(fù)雜性。
若是需要在結(jié)構(gòu)體中使用 Context 的值,最好將需要的值作為結(jié)構(gòu)體的字段傳遞進來。
type MyStruct struct {
// 不推薦的做法
Ctx context.Context
// 推薦的做法
UserID int
}





























