Go語(yǔ)言中的context包到底解決了啥問題?
Go語(yǔ)言,自2009年發(fā)布以來,憑借其簡(jiǎn)潔、高效、并發(fā)能力強(qiáng)等特點(diǎn),迅速在開發(fā)者社區(qū)獲得了廣泛的關(guān)注和應(yīng)用,特別是在服務(wù)器端開發(fā)、云計(jì)算、容器技術(shù)和微服務(wù)架構(gòu)等領(lǐng)域。例如,Docker 和 K8S 等知名的容器技術(shù)都是使用Go語(yǔ)言開發(fā)的。
為什么需要Context包?
認(rèn)識(shí) goroutine
首先讓我們來認(rèn)識(shí)下 goroutine。
Go語(yǔ)言的高并發(fā)、高性能都來源于它的并發(fā)模型:goroutine,就是它,讓開發(fā)者可以輕松地編寫高吞吐量的應(yīng)用程序,這在處理大量并發(fā)請(qǐng)求的服務(wù)器端開發(fā)中尤為重要。
goroutine是Go語(yǔ)言中的輕量級(jí)線程,或者稱為協(xié)程。與操作系統(tǒng)級(jí)別的線程相比,goroutine的創(chuàng)建和銷毀開銷非常小,調(diào)度效率也很高,因此在Go語(yǔ)言中,可以輕松地創(chuàng)建成千上萬(wàn)個(gè)goroutine來處理并發(fā)任務(wù)。
使用goroutine非常簡(jiǎn)單,只需在函數(shù)調(diào)用前加上go關(guān)鍵字即可。例如:
go func() {
// 并發(fā)執(zhí)行的代碼
}()
并發(fā)編程的挑戰(zhàn)
goroutine 雖然讓并發(fā)編程變得非常方便,但也帶來了新的挑戰(zhàn)。
- 超時(shí)控制:許多操作(如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)查詢等)都可能因?yàn)楦鞣N原因變得緩慢甚至無限期掛起。如果沒有合適的超時(shí)控制機(jī)制,這些操作可能會(huì)導(dǎo)致計(jì)算機(jī)資源被長(zhǎng)時(shí)間占用,影響系統(tǒng)的整體性能和響應(yīng)速度。
- 取消操作:某些情況下,某些操作可能需要被取消。例如,當(dāng)用戶取消了一個(gè)正在進(jìn)行的請(qǐng)求,或者當(dāng)某個(gè)前置條件不再滿足時(shí),我們需要能夠及時(shí)地取消正在進(jìn)行的操作,以避免不必要的資源消耗。
- 數(shù)據(jù)傳遞:不同的goroutine之間可能需要共享和傳遞一些上下文信息。例如,在一個(gè)請(qǐng)求的處理過程中,我們可能需要在多個(gè)函數(shù)調(diào)用之間傳遞用戶身份、請(qǐng)求ID等。這些信息需要能夠安全地在多個(gè)goroutine之間傳遞和共享。
這些挑戰(zhàn)在其它語(yǔ)言的并發(fā)編程模型中也是廣泛存在的。
為什么需要context包
為了解決并發(fā)編程中的常見挑戰(zhàn),Go語(yǔ)言引入了context包。context包提供了一種統(tǒng)一的機(jī)制來管理請(qǐng)求的生命周期,傳遞取消信號(hào),設(shè)置超時(shí)時(shí)間,并在不同的goroutine之間傳遞上下文信息。
- 統(tǒng)一管理請(qǐng)求生命周期:context包允許我們?yōu)槊恳粋€(gè)請(qǐng)求創(chuàng)建一個(gè)上下文對(duì)象(上下文通常就翻譯為context),并在請(qǐng)求的整個(gè)生命周期中傳遞這個(gè)上下文對(duì)象。如此,我們就可以在請(qǐng)求結(jié)束時(shí),及時(shí)釋放所有相關(guān)的資源。
- 傳遞取消信號(hào):context包提供了取消信號(hào)的傳遞機(jī)制。我們可以創(chuàng)建一個(gè)可以取消的上下文對(duì)象,并在需要取消操作時(shí)調(diào)用取消函數(shù),通知所有相關(guān)的goroutine取消操作。當(dāng)然這不是自動(dòng)發(fā)生的,還需要我們編寫代碼進(jìn)行判斷。
- 設(shè)置超時(shí)時(shí)間:context包還提供了超時(shí)控制的機(jī)制。我們可以為操作設(shè)置超時(shí)時(shí)間,并在操作超時(shí)后自動(dòng)取消操作。
- 傳遞和共享數(shù)據(jù):context包還提供了一種安全的方式在不同的goroutine之間傳遞和共享上下文信息。我們可以將一些關(guān)鍵數(shù)據(jù)存儲(chǔ)在上下文對(duì)象中,并在不同的函數(shù)調(diào)用中傳遞這個(gè)上下文對(duì)象,從而實(shí)現(xiàn)數(shù)據(jù)的安全共享。
context包的使用方法
HTTP請(qǐng)求處理中context應(yīng)用
讓我們先通過一個(gè)例子來感受下 context 包的強(qiáng)大能力。
在Go的net/http包中,每個(gè)HTTP請(qǐng)求都會(huì)自動(dòng)攜帶一個(gè)context。我們可以通過req.Context()方法獲取這個(gè)context,并在處理請(qǐng)求時(shí)使用它。以下是一個(gè)簡(jiǎn)單的示例。
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
)
// 定義一個(gè)key類型,用于在context中存儲(chǔ)和檢索數(shù)據(jù)
type key string
const (
userIDKey key = "userID"
)
// 定義一個(gè)向控制臺(tái)輸出日志的logger
var 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) {
// 設(shè)置請(qǐng)求的超時(shí)為5秒
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
defer cancel()
// 在context中存儲(chǔ)一些共享數(shù)據(jù),例如用戶ID
ctx = context.WithValue(ctx, userIDKey, "12345")
// 模擬一些工作,將在goroutine中運(yùn)行,通過channel通知完成
done := make(chan struct{})
go func() {
// 從context取出用戶ID,記錄到日志中
userID := ctx.Value(userIDKey).(string)
logger.Println("開始處理:", userID)
time.Sleep(3 * time.Second) // 模擬耗時(shí)操作
close(done)
}()
// 通過select跟蹤context超時(shí)或者工作完成
select {
case <-ctx.Done():
// 請(qǐng)求被取消或超時(shí)
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)
}
}
在這個(gè)示例中,我們?cè)贖TTP處理器中使用 context.WithTimeout 設(shè)置了一個(gè)5秒的超時(shí)。如果請(qǐng)求在5秒內(nèi)沒有完成,context將自動(dòng)取消,處理器會(huì)返回一個(gè)超時(shí)錯(cuò)誤響應(yīng)。如果操作在5秒內(nèi)完成,則返回正常的響應(yīng)。
在這個(gè)例子中,我們還使用了 context 來共享數(shù)據(jù),在創(chuàng)建超時(shí)context之后,我們使用 context.WithValue 在context 中存儲(chǔ)了用戶ID。
ctx = context.WithValue(ctx, userIDKey, "12345")
在處理具體的工作時(shí),我們使用 ctx.Value 從context中檢索共享數(shù)據(jù),打印正在處理的用戶:
userID := ctx.Value(userIDKey).(string)
logger.Println("開始處理:", userID)
在完成后,我們還是使用ctx.Value從context中檢索共享數(shù)據(jù),并將其包含在響應(yīng)中:
userID := ctx.Value(userIDKey).(string)
fmt.Fprintf(w, "Hello, User ID: %s!\n", userID)
基本的context用法
創(chuàng)建 context
在Go語(yǔ)言中,創(chuàng)建一個(gè)context對(duì)象是使用context包的第一步。
在上邊的例子中,我們從http請(qǐng)求中獲取了一個(gè)context,其實(shí)我們也完全可以自己創(chuàng)建一個(gè)新的context,有兩種基本方法:
- context.Background()
context.Background()返回一個(gè)空的context對(duì)象,通常用于整個(gè)應(yīng)用程序的頂級(jí)context,或者在不確定應(yīng)該使用哪個(gè)context的情況下使用。它是一個(gè)常見的根context,所有的派生context都會(huì)基于它。
- context.TODO()
context.TODO()與context.Background()類似,但通常用于你還不確定要使用哪個(gè)context,或者代碼還在開發(fā)過程中,未來可能會(huì)被替換為更具體的context。
傳遞context
我們可以在內(nèi)嵌函數(shù)中直接使用有效范圍之內(nèi)的 contex t實(shí)例,不過更常見的傳遞方法是通過函數(shù)參數(shù)。
在Go語(yǔ)言中,context對(duì)象通常作為函數(shù)的第一個(gè)參數(shù)進(jìn)行傳遞。這種方式確保了context在整個(gè)調(diào)用鏈中被正確傳遞和使用。代碼如下:
func doSomething(ctx context.Context) {
// 在函數(shù)內(nèi)部使用context
}
func main() {
ctx := context.Background()
doSomething(ctx)
}
取消context
context.WithCancel() 函數(shù)返回一個(gè)派生的context和一個(gè)取消函數(shù)。調(diào)用取消函數(shù)會(huì)取消這個(gè)派生的context,并通知所有使用這個(gè)context的goroutine進(jìn)行清理操作。示例代碼如下:
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 模擬一些工作
time.Sleep(2 * time.Second)
// 取消context
cancel()
}()
select {
case <-ctx.Done():
fmt.Println("操作被取消")
}
設(shè)置超時(shí)
上邊http服務(wù)端處理的例子中我們已經(jīng)提供了一種設(shè)置context超時(shí)的方法,另外還有一個(gè)設(shè)置context超時(shí)的方法:context.WithDeadline(),這個(gè)函數(shù)函數(shù)類似于context.WithTimeout(),但它允許你指定一個(gè)具體的時(shí)間點(diǎn)作為截止時(shí)間。代碼示例如下:
deadline := time.Now().Add(3 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel() // 確保在不再需要時(shí)取消context
select {
case <-ctx.Done():
fmt.Println("操作在截止時(shí)間前未完成")
}
context的最佳實(shí)踐
合理設(shè)置超時(shí)時(shí)間
超時(shí)時(shí)間設(shè)置的過長(zhǎng),請(qǐng)求都等著,可能會(huì)消耗過多的計(jì)算資源;設(shè)置的太小,頻繁超時(shí),又會(huì)給用戶帶來不好的使用體驗(yàn)。以下是一些最佳實(shí)踐:
- 根據(jù)業(yè)務(wù)需求設(shè)置超時(shí):不同的業(yè)務(wù)場(chǎng)景對(duì)響應(yīng)時(shí)間的要求不同。根據(jù)具體業(yè)務(wù)需求來設(shè)置超時(shí)時(shí)間,例如用戶請(qǐng)求的超時(shí)可以設(shè)置得較短,而后臺(tái)批量處理任務(wù)的超時(shí)可以設(shè)置得較長(zhǎng)。
- 逐層縮短超時(shí):在多層級(jí)服務(wù)調(diào)用中,通常應(yīng)該逐層縮短超時(shí)時(shí)間。比如,頂層請(qǐng)求的超時(shí)時(shí)間為10秒,調(diào)用的子服務(wù)可以設(shè)置為8秒,再調(diào)用的子服務(wù)可以設(shè)置為6秒,以確保在超時(shí)前有足夠的時(shí)間處理和傳遞錯(cuò)誤。
- 考慮網(wǎng)絡(luò)延遲和重試機(jī)制:在分布式系統(tǒng)中,網(wǎng)絡(luò)延遲和重試機(jī)制會(huì)影響實(shí)際的處理時(shí)間。設(shè)置超時(shí)時(shí)應(yīng)考慮這些因素,避免超時(shí)時(shí)間過短導(dǎo)致頻繁的重試。
避免context的濫用
context包的主要目的是在請(qǐng)求的生命周期中傳遞取消信號(hào)、超時(shí)和共享數(shù)據(jù),不要傳遞過多的業(yè)務(wù)數(shù)據(jù),以下是一些建議:
- 不將context用于傳遞業(yè)務(wù)數(shù)據(jù):context應(yīng)該只用于傳遞請(qǐng)求的控制信息(如取消信號(hào)、超時(shí)和trace信息),不應(yīng)該用于傳遞業(yè)務(wù)數(shù)據(jù)。
- 不將context存儲(chǔ)在結(jié)構(gòu)體中:context是臨時(shí)性的,不應(yīng)該存儲(chǔ)在結(jié)構(gòu)體中以避免內(nèi)存泄漏和不必要的復(fù)雜性。
- 及時(shí)取消context:使用context.WithCancel、context.WithTimeout或context.WithDeadline創(chuàng)建的context應(yīng)該及時(shí)調(diào)用取消函數(shù),以釋放資源。
- 避免頻繁創(chuàng)建context:創(chuàng)建和取消context本身的開銷相對(duì)較小,但頻繁的創(chuàng)建和取消操作仍然會(huì)對(duì)性能產(chǎn)生一定影響,特別是在高并發(fā)場(chǎng)景下。在設(shè)計(jì)系統(tǒng)時(shí),盡量減少不必要的context創(chuàng)建操作,可以復(fù)用已有的context,避免在每個(gè)函數(shù)調(diào)用中都創(chuàng)建新的context。
有的同學(xué)可能會(huì)有疑問:context.WithTimeout 或 context.WithDeadline 創(chuàng)建的context等著超時(shí)或者正常處理完成不就可以了嗎?
其實(shí) context.WithTimeout 和 context.WithDeadline,這兩個(gè)函數(shù)內(nèi)部也是通過 WithCancel 實(shí)現(xiàn)的,因此也會(huì)返回一個(gè) cancel 函數(shù)。盡管當(dāng)超時(shí)或截止日期到達(dá)時(shí),context會(huì)自動(dòng)“過期”,不過調(diào)用 cancel 函數(shù)仍然是一個(gè)好習(xí)慣,因?yàn)樗梢粤⒓赐V谷魏我蕾囉诖松舷挛牡恼谶M(jìn)行的操作,而不僅僅等待它們自然發(fā)現(xiàn)上下文已過期。
與其他包的結(jié)合使用
我們不僅可以在自己編寫的代碼中使用context,很多標(biāo)準(zhǔn)庫(kù)也提供了context的支持,這樣可以更好的管理請(qǐng)求和資源。上邊的示例中已經(jīng)演示了與net/http包結(jié)合,我們?cè)倏聪耫atabase/sql的例子:
package main
import (
"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)建一個(gè)帶超時(shí)的 context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 查詢數(shù)據(jù)庫(kù)時(shí),傳入這個(gè)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)
}
}
通過結(jié)合使用context包和其他標(biāo)準(zhǔn)庫(kù),我們就可以更好地管理每個(gè)請(qǐng)求的生命周期和使用的各種資源,提高整個(gè)系統(tǒng)的穩(wěn)定性和可維護(hù)性。