Golang 中的 Context 包
今天,我們將討論 Go 編程中非常重要的一個(gè)主題:context 包。如果你現(xiàn)在覺得它很令人困惑,不用擔(dān)心 — 在本文結(jié)束時(shí),你將像專家一樣處理 context!
想象一下,你在一個(gè)主題公園,興奮地準(zhǔn)備搭乘一座巨大的過山車。但有個(gè)問題:排隊(duì)的人非常多,而且公園快要關(guān)門,你只有一個(gè)小時(shí)的時(shí)間。你會(huì)怎么辦?嗯,你可能會(huì)等一會(huì)兒,但不會(huì)等一個(gè)小時(shí),對(duì)吧?如果你等了 30 分鐘還沒有到前面,你會(huì)離開隊(duì)伍去嘗試其他游樂設(shè)施。這就是我們所謂的 '超時(shí)'。
現(xiàn)在,想象一下,你還在排隊(duì),突然下起了傾盆大雨。過山車的操作員決定關(guān)閉過山車。你不會(huì)繼續(xù)排隊(duì)等待根本不會(huì)發(fā)生的事情,對(duì)吧?你會(huì)立刻離開隊(duì)伍。這就是我們所謂的 '取消'。
在編程世界中,我們經(jīng)常面臨類似的情況。我們要求程序執(zhí)行可能需要很長(zhǎng)時(shí)間或需要因某種原因停止的任務(wù)。這就是 context 包發(fā)揮作用的地方。它允許我們優(yōu)雅地處理這些超時(shí)和取消。
它是如何工作的
(1) 創(chuàng)建上下文:我們首先創(chuàng)建一個(gè)上下文。這就像排隊(duì)等待過山車一樣。
ctx := context.Background() // This gives you an empty context
(2) 設(shè)置超時(shí):接下來(lái),我們可以在上下文中設(shè)置超時(shí)。這就好比你決定在排隊(duì)多久后放棄并去嘗試其他游樂設(shè)施。
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) 檢查超時(shí):現(xiàn)在,我們可以使用上下文來(lái)檢查是否等待時(shí)間太長(zhǎng),是否應(yīng)該停止我們的任務(wù)。這就好比在排隊(duì)等待時(shí)看看手表。
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) 取消上下文:最后,如果出于某種原因需要停止任務(wù),我們可以取消上下文。這就好比聽到因下雨而宣布過山車關(guān)閉。
cancel() // We call the cancel function we got when we created our context with timeout
示例 1:慢速數(shù)據(jù)庫(kù)查詢
想象一下構(gòu)建一個(gè)從數(shù)據(jù)庫(kù)中獲取用戶數(shù)據(jù)的Web應(yīng)用程序。有時(shí),數(shù)據(jù)庫(kù)響應(yīng)較慢,你不希望用戶永遠(yuǎn)等下去。在這種情況下,你可以使用帶有超時(shí)的上下文。
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
}
在這個(gè)示例中,如果數(shù)據(jù)庫(kù)查詢花費(fèi)超過3秒的時(shí)間,上下文將被取消,db.QueryRowContext 應(yīng)返回一個(gè)錯(cuò)誤。
示例 2:網(wǎng)頁(yè)抓取
假設(shè)你正在編寫一個(gè)用于從網(wǎng)站抓取數(shù)據(jù)的程序。然而,該網(wǎng)站有時(shí)響應(yīng)較慢,或者根本不響應(yīng)。你可以使用上下文來(lái)防止你的程序陷入困境。
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
}
在這個(gè)示例中,如果從網(wǎng)站獲取數(shù)據(jù)超過5秒,上下文將被取消,http.DefaultClient.Do 應(yīng)該返回一個(gè)錯(cuò)誤。
示例3:長(zhǎng)時(shí)間運(yùn)行的任務(wù)
假設(shè)你有一個(gè)執(zhí)行長(zhǎng)時(shí)間運(yùn)行任務(wù)的程序,但你希望能夠在程序接收到關(guān)閉信號(hào)時(shí)停止任務(wù)。這在一個(gè) Web 服務(wù)器中可能會(huì)很有用,當(dāng)關(guān)閉時(shí)必須停止提供請(qǐng)求并進(jìn)行清理。
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)
}
在這個(gè)示例中,當(dāng)程序接收到關(guān)閉信號(hào)時(shí),它會(huì)取消上下文,這會(huì)導(dǎo)致 doTask 在 <-ctx.Done() 上接收到信號(hào)。
示例4:HTTP 服務(wù)器
假設(shè)你正在構(gòu)建一個(gè)處理傳入請(qǐng)求的 HTTP 服務(wù)器。一些請(qǐng)求可能需要很長(zhǎng)時(shí)間來(lái)處理,你希望設(shè)置一個(gè)最長(zhǎng)處理時(shí)間限制。
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)
在這個(gè)示例中,如果操作需要超過2秒的時(shí)間,上下文將被取消,并且服務(wù)器將響應(yīng)“操作超時(shí)”。
示例5:同步多個(gè) Goroutines
假設(shè)你正在編寫一個(gè)程序,使用 Goroutines 并發(fā)執(zhí)行多個(gè)任務(wù)。如果其中一個(gè)任務(wù)失敗,你希望取消所有其他任務(wù)。
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)
}
在這個(gè)示例中,當(dāng)上下文被取消時(shí),仍在運(yùn)行的任何任務(wù)都將收到 <-ctx.Done(),從而允許它們進(jìn)行清理并退出。
仍然在嘗試?yán)斫鈫幔?/h4>
當(dāng)我第一次接觸上下文時(shí),我感到非常困惑,我提出了一個(gè)問題,即如果 select 前面的命令花費(fèi)太長(zhǎng)時(shí)間,那么我們永遠(yuǎn)無(wú)法檢測(cè)到 取消,這是一個(gè)合理的問題。因此,我準(zhǔn)備了另一個(gè)示例來(lái)詳細(xì)解釋這種情況。
package main
import (
"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ù)時(shí)間已過。這意味著 select 語(yǔ)句不會(huì)被執(zhí)行,直到休眠時(shí)間已經(jīng)過去。
然而,上下文的取消與 goroutine 內(nèi)的執(zhí)行是獨(dú)立的。如果上下文的截止時(shí)間被超過或其 cancel() 函數(shù)被調(diào)用,它的 Done() 通道將被關(guān)閉。
在主 goroutine 中,您有另一個(gè) select 語(yǔ)句,它將立即檢測(cè)上下文的 Done() 通道是否已關(guān)閉,并在不等待 expensiveCalculation goroutine 完成休眠的情況下打印 **"Calculation cancelled"**。
也就是說,expensiveCalculation goroutine 將在休眠后繼續(xù)執(zhí)行,它將在嘗試寫入 resultChan 之前檢查上下文是否已被取消。如果已被取消,它將立即返回。這是為了避免潛在的死鎖,如果沒有其他goroutine從 resultChan 讀取。
如果需要昂貴的計(jì)算(在本例中由 time.Sleep 模擬)在取消時(shí)立即停止,您必須設(shè)計(jì)計(jì)算以周期性地檢查上下文是否已取消。這通常在需要將計(jì)算分解為較小部分的情況下使用循環(huán)。如果計(jì)算不能分解,并需要一次運(yùn)行完畢,那么很遺憾,在 Go 中無(wú)法提前停止它。