Go語言,自2009年發(fā)布以來,憑借其簡潔、高效、并發(fā)能力強等特點,迅速在開發(fā)者社區(qū)獲得了廣泛的關注和應用,特別是在服務器端開發(fā)、云計算、容器技術和微服務架構等領域。例如,Docker 和 K8S 等知名的容器技術都是使用Go語言開發(fā)的。
首先讓我們來認識下 goroutine。
Go語言的高并發(fā)、高性能都來源于它的并發(fā)模型:goroutine,就是它,讓開發(fā)者可以輕松地編寫高吞吐量的應用程序,這在處理大量并發(fā)請求的服務器端開發(fā)中尤為重要。
goroutine是Go語言中的輕量級線程,或者稱為協(xié)程。與操作系統(tǒng)級別的線程相比,goroutine的創(chuàng)建和銷毀開銷非常小,調(diào)度效率也很高,因此在Go語言中,可以輕松地創(chuàng)建成千上萬個goroutine來處理并發(fā)任務。
使用goroutine非常簡單,只需在函數(shù)調(diào)用前加上go關鍵字即可。例如:
go func() { // 并發(fā)執(zhí)行的代碼}()
goroutine 雖然讓并發(fā)編程變得非常方便,但也帶來了新的挑戰(zhàn)。
這些挑戰(zhàn)在其它語言的并發(fā)編程模型中也是廣泛存在的。
為了解決并發(fā)編程中的常見挑戰(zhàn),Go語言引入了context包。context包提供了一種統(tǒng)一的機制來管理請求的生命周期,傳遞取消信號,設置超時時間,并在不同的goroutine之間傳遞上下文信息。
讓我們先通過一個例子來感受下 context 包的強大能力。
在Go的net/http包中,每個HTTP請求都會自動攜帶一個context。我們可以通過req.Context()方法獲取這個context,并在處理請求時使用它。以下是一個簡單的示例。
package mainimport ( "context" "fmt" "log" "net/http" "os" "time")// 定義一個key類型,用于在context中存儲和檢索數(shù)據(jù)type key stringconst ( userIDKey key = "userID")// 定義一個向控制臺輸出日志的loggervar logger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)func main() { http.HandleFunc("/hello", helloHandler) http.ListenAndServe(":8080", nil)}func helloHandler(w http.ResponseWriter, req *http.Request) { // 設置請求的超時為5秒 ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) defer cancel() // 在context中存儲一些共享數(shù)據(jù),例如用戶ID ctx = context.WithValue(ctx, userIDKey, "12345") // 模擬一些工作,將在goroutine中運行,通過channel通知完成 done := make(chan struct{}) go func() { // 從context取出用戶ID,記錄到日志中 userID := ctx.Value(userIDKey).(string) logger.Println("開始處理:", userID) time.Sleep(3 * time.Second) // 模擬耗時操作 close(done) }() // 通過select跟蹤context超時或者工作完成 select { case <-ctx.Done(): // 請求被取消或超時 http.Error(w, "Request canceled or timed out", http.StatusRequestTimeout) case <-done: // 操作完成,從context中取出用戶ID,返回給調(diào)用方 userID := ctx.Value(userIDKey).(string) fmt.Fprintf(w, "Hello, User ID: %s!/n", userID) }}
在這個示例中,我們在HTTP處理器中使用 context.WithTimeout 設置了一個5秒的超時。如果請求在5秒內(nèi)沒有完成,context將自動取消,處理器會返回一個超時錯誤響應。如果操作在5秒內(nèi)完成,則返回正常的響應。
在這個例子中,我們還使用了 context 來共享數(shù)據(jù),在創(chuàng)建超時context之后,我們使用 context.WithValue 在context 中存儲了用戶ID。
ctx = context.WithValue(ctx, userIDKey, "12345")
在處理具體的工作時,我們使用 ctx.Value 從context中檢索共享數(shù)據(jù),打印正在處理的用戶:
userID := ctx.Value(userIDKey).(string)logger.Println("開始處理:", userID)
在完成后,我們還是使用ctx.Value從context中檢索共享數(shù)據(jù),并將其包含在響應中:
userID := ctx.Value(userIDKey).(string)fmt.Fprintf(w, "Hello, User ID: %s!/n", userID)
在Go語言中,創(chuàng)建一個context對象是使用context包的第一步。
在上邊的例子中,我們從http請求中獲取了一個context,其實我們也完全可以自己創(chuàng)建一個新的context,有兩種基本方法:
context.Background()返回一個空的context對象,通常用于整個應用程序的頂級context,或者在不確定應該使用哪個context的情況下使用。它是一個常見的根context,所有的派生context都會基于它。
context.TODO()與context.Background()類似,但通常用于你還不確定要使用哪個context,或者代碼還在開發(fā)過程中,未來可能會被替換為更具體的context。
我們可以在內(nèi)嵌函數(shù)中直接使用有效范圍之內(nèi)的 contex t實例,不過更常見的傳遞方法是通過函數(shù)參數(shù)。
在Go語言中,context對象通常作為函數(shù)的第一個參數(shù)進行傳遞。這種方式確保了context在整個調(diào)用鏈中被正確傳遞和使用。代碼如下:
func doSomething(ctx context.Context) { // 在函數(shù)內(nèi)部使用context}func main() { ctx := context.Background() doSomething(ctx)}
context.WithCancel() 函數(shù)返回一個派生的context和一個取消函數(shù)。調(diào)用取消函數(shù)會取消這個派生的context,并通知所有使用這個context的goroutine進行清理操作。示例代碼如下:
ctx, cancel := context.WithCancel(context.Background())go func() { // 模擬一些工作 time.Sleep(2 * time.Second) // 取消context cancel()}()select {case <-ctx.Done(): fmt.Println("操作被取消")}
上邊http服務端處理的例子中我們已經(jīng)提供了一種設置context超時的方法,另外還有一個設置context超時的方法:context.WithDeadline(),這個函數(shù)函數(shù)類似于context.WithTimeout(),但它允許你指定一個具體的時間點作為截止時間。代碼示例如下:
deadline := time.Now().Add(3 * time.Second)ctx, cancel := context.WithDeadline(context.Background(), deadline)defer cancel() // 確保在不再需要時取消contextselect {case <-ctx.Done(): fmt.Println("操作在截止時間前未完成")}
超時時間設置的過長,請求都等著,可能會消耗過多的計算資源;設置的太小,頻繁超時,又會給用戶帶來不好的使用體驗。以下是一些最佳實踐:
context包的主要目的是在請求的生命周期中傳遞取消信號、超時和共享數(shù)據(jù),不要傳遞過多的業(yè)務數(shù)據(jù),以下是一些建議:
有的同學可能會有疑問:context.WithTimeout 或 context.WithDeadline 創(chuàng)建的context等著超時或者正常處理完成不就可以了嗎?
其實 context.WithTimeout 和 context.WithDeadline,這兩個函數(shù)內(nèi)部也是通過 WithCancel 實現(xiàn)的,因此也會返回一個 cancel 函數(shù)。盡管當超時或截止日期到達時,context會自動“過期”,不過調(diào)用 cancel 函數(shù)仍然是一個好習慣,因為它可以立即停止任何依賴于此上下文的正在進行的操作,而不僅僅等待它們自然發(fā)現(xiàn)上下文已過期。
我們不僅可以在自己編寫的代碼中使用context,很多標準庫也提供了context的支持,這樣可以更好的管理請求和資源。上邊的示例中已經(jīng)演示了與net/http包結合,我們再看下database/sql的例子:
package mainimport ( "context" "database/sql" "log" "time" _ "github.com/go-sql-driver/mysql")func main() { db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname") if err != nil { log.Fatal(err) } defer db.Close() // 創(chuàng)建一個帶超時的 context ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // 查詢數(shù)據(jù)庫時,傳入這個context rows, err := db.QueryContext(ctx, "SELECT * FROM users") if err != nil { log.Println("Query error:", err) return } defer rows.Close() for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { log.Println("Scan error:", err) return } log.Printf("User: %d, Name: %s/n", id, name) } if err := rows.Err(); err != nil { log.Println("Rows error:", err) }}
通過結合使用context包和其他標準庫,我們就可以更好地管理每個請求的生命周期和使用的各種資源,提高整個系統(tǒng)的穩(wěn)定性和可維護性。
本文鏈接:http://www.tebozhan.com/showinfo-26-92097-0.htmlGo語言中的context包到底解決了啥問題?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 聯(lián)發(fā)科天璣 9300 系列亮相 COMPUTEX 2024,加速生成式 AI 手機時代到來