在 Go 語言中,context(上下文)是一個非常重要的概念。
它主要用于在多個 goroutine 之間傳遞請求特定任務的截止日期、取消信號以及其他請求范圍的值。3. Context 的取消與超時
本文將探討 Go 語言中context的用法,從基礎概念到實際應用,將全面了解上下文的使用方法。
主要內容包括
什么是 Context(上下文)
Context 的基本用法:創建與傳遞
Context 的取消與超時
Context 的值傳遞
實際應用場景:HTTP 請求的 Context 使用
數據庫操作中的 Context 應用
自定義 Context 的實現
Context 的生命周期管理
Context 的注意事項
在 Go 語言中,context(上下文)是一個接口,定義了四個方法:Deadline()、Done()、Err()和Value(key interface{})。
它主要用于在 goroutine 之間傳遞請求的截止日期、取消信號以及其他請求范圍的值。
package mainimport ( "context" "fmt")func main() { // 創建一個空的Context ctx := context.Background() // 創建一個帶有取消信號的Context _, cancel := context.WithCancel(ctx) defer cancel() // 延遲調用cancel,確保在函數退出時取消Context fmt.Println("Context創建成功")}
以上代碼演示了如何創建一個空的context和一個帶有取消信號的context。
使用context.WithCancel(parent)可以創建一個帶有取消信號的子context。
在調用cancel函數時,所有基于該context的操作都會收到取消信號。
package mainimport ( "context" "fmt" "time")func worker(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("收到取消信號,任務結束") return default: fmt.Println("正在執行任務...") 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() // 發送取消信號 time.Sleep(1 * time.Second)}
在上面例子中,創建了一個帶有取消信號的context,并將它傳遞給一個 goroutine 中的任務。
調用cancel函數,可以發送取消信號,中斷任務的執行。
package mainimport ( "context" "fmt" "time")func main() { ctx := context.Background() ctxWithCancel, cancel := context.WithCancel(ctx) go func() { select { case <-ctxWithCancel.Done(): fmt.Println("收到取消信號,任務結束") case <-time.After(2 * time.Second): fmt.Println("任務完成") } }() time.Sleep(1 * time.Second) cancel() // 發送取消信號 time.Sleep(1 * time.Second)}
在這個例子中,使用time.After函數模擬一個長時間運行的任務。
如果任務在 2 秒內完成,就打印“任務完成”,否則在接收到取消信號后打印“收到取消信號,任務結束”。
package mainimport ( "context" "fmt" "time")func main() { ctx := context.Background() ctxWithTimeout, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() select { case <-ctxWithTimeout.Done(): fmt.Println("超時,任務結束") case <-time.After(1 * time.Second): fmt.Println("任務完成") }}
在上面例子中,使用context.WithTimeout(parent, duration)函數創建了一個在 2 秒后自動取消的context
如果任務在 1 秒內完成,就打印“任務完成”,否則在 2 秒后打印“超時,任務結束”。
package mainimport ( "context" "fmt")type key stringfunc main() { ctx := context.WithValue(context.Background(), key("name"), "Alice") value := ctx.Value(key("name")) fmt.Println("Name:", value)}
在上面例子中,使用context.WithValue(parent, key, value)函數在context中傳遞了一個鍵值對。
使用ctx.Value(key)函數可以獲取傳遞的值。
package mainimport ( "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結構體中的Context()方法獲取到請求的context,并在處理請求時設置了 3 秒的超時時間。
如果請求在 3 秒內完成,就返回“Hello, World!”,否則返回一個錯誤。
package mainimport ( "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)函數為每個請求設置了 2 秒的超時時間。
如果請求在 2 秒內完成,就返回“Hello, World!”,否則返回一個錯誤。
package mainimport ( "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() // 處理查詢結果}
在上面例子中,使用context.WithTimeout(parent, duration)函數為數據庫查詢設置了 2 秒的超時時間,確保在 2 秒內完成查詢操作。
如果查詢超時,程序會收到context的取消信號,從而中斷數據庫查詢。
package mainimport ( "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("接收到取消信號,回滾事務") tx.Rollback() }() // 執行長時間運行的事務操作 // ... err = tx.Commit() if err != nil { fmt.Println("提交事務出錯:", err) return } fmt.Println("事務提交成功")}
在這個例子中,使用context.WithTimeout(parent, duration)函數為數據庫事務設置了 2 秒的超時時間。
同時使用 goroutine 監聽context的取消信號。
在 2 秒內事務沒有完成,程序會收到context的取消信號,從而回滾事務。
package mainimport ( "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)}
在這個例子中,實現了一個自定義的MyContext類型,它包含了一個RequestID字段。
通過WithRequestID函數,可以在原有的context上附加一個RequestID值,然后在需要的時候獲取這個值。
自定義context類型的應用場景非常廣泛,比如在微服務架構中,
可在context中加入一些額外的信息,比如用戶 ID、請求來源等,以便在服務調用鏈路中傳遞這些信息。
context.Context 是不可變的,一旦創建就不能修改。它的值在傳遞時是安全的,可以被多個 goroutine 共享。
在一個請求處理的生命周期內,通常會創建一個頂級的 Context,然后從這個頂級的 Context 派生出子 Context,傳遞給具體的處理函數。
當請求處理完成或者發生錯誤時,應該主動調用 context.WithCancel 或 context.WithTimeout 等函數創建的 Context 的 Cancel 函數來通知子 goroutines 停止工作。
這樣可以確保資源被及時釋放,避免 goroutine 泄漏。
func handleRequest(ctx context.Context) { // 派生一個新的 Context,設置超時時間 ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() // 確保在函數退出時調用 cancel,防止資源泄漏 // 在這個新的 Context 下進行操作 // ...}
在函數之間傳遞 Context 時,確保傳遞的是必要的最小信息。避免在函數間傳遞整個 Context,而是選擇傳遞 Context 的衍生物。
如 context.WithValue 創建的 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))})}
避免在函數的參數列表中傳遞 context.Context,除非確實需要用到它。
如果函數的邏輯只需要使用 Context 的部分功能,那么最好只傳遞它需要的具體信息,而不是整個 Context。
// 不推薦的做法func processRequest(ctx context.Context) { // ...}// 推薦的做法func processRequest(userID int, timeout time.Duration) { // 使用 userID 和 timeout,而不是整個 Context}
不要在結構體中保存 context.Context,因為它會增加結構體的復雜性。
若是需要在結構體中使用 Context 的值,最好將需要的值作為結構體的字段傳遞進來。
type MyStruct struct { // 不推薦的做法 Ctx context.Context // 推薦的做法 UserID int}
本文鏈接:http://www.tebozhan.com/showinfo-26-16279-0.htmlGo語言高級特性:Context深入解讀
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com