今天,我們將討論 Go 編程中非常重要的一個主題:context 包。如果你現(xiàn)在覺得它很令人困惑,不用擔心 — 在本文結束時,你將像專家一樣處理 context!
想象一下,你在一個主題公園,興奮地準備搭乘一座巨大的過山車。但有個問題:排隊的人非常多,而且公園快要關門,你只有一個小時的時間。你會怎么辦?嗯,你可能會等一會兒,但不會等一個小時,對吧?如果你等了 30 分鐘還沒有到前面,你會離開隊伍去嘗試其他游樂設施。這就是我們所謂的 '超時'。
現(xiàn)在,想象一下,你還在排隊,突然下起了傾盆大雨。過山車的操作員決定關閉過山車。你不會繼續(xù)排隊等待根本不會發(fā)生的事情,對吧?你會立刻離開隊伍。這就是我們所謂的 '取消'。
在編程世界中,我們經(jīng)常面臨類似的情況。我們要求程序執(zhí)行可能需要很長時間或需要因某種原因停止的任務。這就是 context 包發(fā)揮作用的地方。它允許我們優(yōu)雅地處理這些超時和取消。
(1) 創(chuàng)建上下文:我們首先創(chuàng)建一個上下文。這就像排隊等待過山車一樣。
ctx := context.Background() // This gives you an empty context
(2) 設置超時:接下來,我們可以在上下文中設置超時。這就好比你決定在排隊多久后放棄并去嘗試其他游樂設施。
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*10) // Wait for 10 seconds// Don't forget to call cancel when you're done, or else you might leak resources!defer cancel()
(3) 檢查超時:現(xiàn)在,我們可以使用上下文來檢查是否等待時間太長,是否應該停止我們的任務。這就好比在排隊等待時看看手表。
select {case <-time.After(time.Second * 15): // This task takes 15 seconds fmt.Println("Finished the task")case <-ctxWithTimeout.Done(): fmt.Println("We've waited too long, let's move on!") // We only wait for 10 seconds}
(4) 取消上下文:最后,如果出于某種原因需要停止任務,我們可以取消上下文。這就好比聽到因下雨而宣布過山車關閉。
cancel() // We call the cancel function we got when we created our context with timeout
想象一下構建一個從數(shù)據(jù)庫中獲取用戶數(shù)據(jù)的Web應用程序。有時,數(shù)據(jù)庫響應較慢,你不希望用戶永遠等下去。在這種情況下,你可以使用帶有超時的上下文。
func getUser(ctx context.Context, id int) (*User, error) { // Create a new context that will be cancelled if it takes more than 3 seconds ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // Assume db.QueryRowContext is a function that executes a SQL query and returns a row row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id) var name string if err := row.Scan(&name); err != nil { return nil, err } return &User{Name: name}, nil}
在這個示例中,如果數(shù)據(jù)庫查詢花費超過3秒的時間,上下文將被取消,db.QueryRowContext 應返回一個錯誤。
假設你正在編寫一個用于從網(wǎng)站抓取數(shù)據(jù)的程序。然而,該網(wǎng)站有時響應較慢,或者根本不響應。你可以使用上下文來防止你的程序陷入困境。
func scrapeWebsite(ctx context.Context, url string) (*html.Node, error) { // Create a new context that will be cancelled if it takes more than 5 seconds ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() // Create a request with the context req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } // Execute the request resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Parse the response body as HTML return html.Parse(resp.Body), nill}
在這個示例中,如果從網(wǎng)站獲取數(shù)據(jù)超過5秒,上下文將被取消,http.DefaultClient.Do 應該返回一個錯誤。
假設你有一個執(zhí)行長時間運行任務的程序,但你希望能夠在程序接收到關閉信號時停止任務。這在一個 Web 服務器中可能會很有用,當關閉時必須停止提供請求并進行清理。
func doTask(ctx context.Context) { for { select { case <-time.After(1 * time.Second): // The task is done, we're ready to exit fmt.Println("Task is done") return case <-ctx.Done(): // The context was cancelled from the outside, clean up and exit fmt.Println("Got cancel signal, cleaning up") return } }}func main() { // Create a new context ctx, cancel := context.WithCancel(context.Background()) // Start the task in a goroutine go doTask(ctx) // Wait for a shutdown signal <-getShutdownSignal() // Cancel the context, which will stop the task cancel() // Wait for a bit to allow the task to clean up time.Sleep(1 * time.Second)}
在這個示例中,當程序接收到關閉信號時,它會取消上下文,這會導致 doTask 在 <-ctx.Done() 上接收到信號。
假設你正在構建一個處理傳入請求的 HTTP 服務器。一些請求可能需要很長時間來處理,你希望設置一個最長處理時間限制。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() // Simulate a long-running operation select { case <-time.After(3 * time.Second): w.Write([]byte("Operation finished.")) case <-ctx.Done(): w.Write([]byte("Operation timed out.")) }})http.ListenAndServe(":8080", nil)
在這個示例中,如果操作需要超過2秒的時間,上下文將被取消,并且服務器將響應“操作超時”。
假設你正在編寫一個程序,使用 Goroutines 并發(fā)執(zhí)行多個任務。如果其中一個任務失敗,你希望取消所有其他任務。
func doTask(ctx context.Context, id int) { select { case <-time.After(time.Duration(rand.Intn(4)) * time.Second): fmt.Printf("Task %v finished./n", id) case <-ctx.Done(): fmt.Printf("Task %v cancelled./n", id) }}func main() { ctx, cancel := context.WithCancel(context.Background()) for i := 1; i <= 5; i++ { go doTask(ctx, i) } // Cancel the context after 2 seconds time.Sleep(2 * time.Second) cancel() // Give the tasks some time to finish up time.Sleep(1 * time.Second)}
在這個示例中,當上下文被取消時,仍在運行的任何任務都將收到 <-ctx.Done(),從而允許它們進行清理并退出。
當我第一次接觸上下文時,我感到非常困惑,我提出了一個問題,即如果 select 前面的命令花費太長時間,那么我們永遠無法檢測到 取消,這是一個合理的問題。因此,我準備了另一個示例來詳細解釋這種情況。
package mainimport ( "context" "fmt" "math/rand" "time")func expensiveCalculation(ctx context.Context, resultChan chan<- int) { // Simulate a long-running calculation rand.Seed(time.Now().UnixNano()) sleepTime := time.Duration(rand.Intn(20)+1) * time.Second fmt.Printf("Calculation will take %s to complete/n", sleepTime) time.Sleep(sleepTime) select { case <-ctx.Done(): // Context was cancelled, don't write to the channel return default: // Write the result to the channel resultChan <- 42 // replace with your actual calculation result }}func main() { // Create a context that will be cancelled after 10 seconds ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // The cancel should be deferred so resources are cleaned up resultChan := make(chan int) // Start the expensive calculation in a separate goroutine go expensiveCalculation(ctx, resultChan) // Wait for either the result or the context to be done select { case res := <-resultChan: // Got the result fmt.Printf("Calculation completed with result: %d/n", res) case <-ctx.Done(): // Context was cancelled fmt.Println("Calculation cancelled") }}
time.Sleep(sleepTime) 命令是阻塞的,將暫停 goroutine 的執(zhí)行,直到指定的持續(xù)時間已過。這意味著 select 語句不會被執(zhí)行,直到休眠時間已經(jīng)過去。
然而,上下文的取消與 goroutine 內的執(zhí)行是獨立的。如果上下文的截止時間被超過或其 cancel() 函數(shù)被調用,它的 Done() 通道將被關閉。
在主 goroutine 中,您有另一個 select 語句,它將立即檢測上下文的 Done() 通道是否已關閉,并在不等待 expensiveCalculation goroutine 完成休眠的情況下打印 **"Calculation cancelled"**。
也就是說,expensiveCalculation goroutine 將在休眠后繼續(xù)執(zhí)行,它將在嘗試寫入 resultChan 之前檢查上下文是否已被取消。如果已被取消,它將立即返回。這是為了避免潛在的死鎖,如果沒有其他goroutine從 resultChan 讀取。
如果需要昂貴的計算(在本例中由 time.Sleep 模擬)在取消時立即停止,您必須設計計算以周期性地檢查上下文是否已取消。這通常在需要將計算分解為較小部分的情況下使用循環(huán)。如果計算不能分解,并需要一次運行完畢,那么很遺憾,在 Go 中無法提前停止它。
本文鏈接:http://www.www897cc.com/showinfo-26-24320-0.htmlGolang 中的 Context 包
聲明:本網(wǎng)頁內容旨在傳播知識,若有侵權等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com