當(dāng)進(jìn)行 Goroutine 編程時(shí),Go 語言中的 Context(上下文)是一個(gè)非常重要的概念。它可以用于在不同的 Goroutine 之間傳遞請求特定值、取消信號以及超時(shí)截止日期等數(shù)據(jù),以協(xié)調(diào) Goroutine 之間的操作。
在本文中,我們將深入介紹 Go 語言中的各種 context,包括它們的含義、區(qū)別以及最佳實(shí)踐。
Context 的含義
Context 是 Go 語言中的一個(gè)接口類型,它定義了在 Goroutine 之間傳遞請求相關(guān)數(shù)據(jù)的方法。Context 接口類型的定義如下:
type Context interface {
// 返回與此上下文關(guān)聯(lián)的取消函數(shù)。
Done() <-chan struct{}
// 返回此上下文的截止時(shí)間(如果有)。
// 如果沒有截止時(shí)間,則ok為false。
Deadline() (deadline time.Time, ok bool)
// 返回此上下文的鍵值對數(shù)據(jù)。
Value(key interface{}) interface{}
}
Context 接口包含三個(gè)方法:
-
Done()方法返回一個(gè)只讀的 channel,當(dāng) context 被取消或者超時(shí)截止日期到達(dá)時(shí),該 channel 會被關(guān)閉。當(dāng)接收到該 channel 關(guān)閉的信號時(shí),就意味著該 context 被取消。 -
Deadline()方法返回 context 的超時(shí)截止日期,如果沒有設(shè)置超時(shí)截止日期,則返回false。當(dāng)時(shí)間達(dá)到超時(shí)截止日期時(shí),context 會自動被取消。 -
Value()方法用于在 context 中存儲和獲取鍵值對數(shù)據(jù)。該方法是非線程安全的。
Context 的類型
Go 語言中常用的 Context 類型有以下幾種
-
context.Background()Background context 是 Context 接口的一個(gè)默認(rèn)實(shí)現(xiàn),它沒有任何值,也不會被取消。當(dāng)沒有更合適的 context 實(shí)例時(shí),可以使用 background context。 -
context.TODO()TODO context 是 Context 接口的一個(gè)默認(rèn)實(shí)現(xiàn),它和 background context 類似,但是它是一個(gè)標(biāo)記未完成工作的 context,用于暫時(shí)占位,待后續(xù)替換為真正的 context 實(shí)例。 -
context.WithCancel(parent)WithCancel 函數(shù)可以派生一個(gè)子 context,同時(shí)返回一個(gè)取消函數(shù),用于在需要的時(shí)候取消該 context。當(dāng)父 context 被取消或者取消函數(shù)被調(diào)用時(shí),子 context 也會被取消。 -
context.WithDeadline(parent, deadline)WithDeadline 函數(shù)可以派生一個(gè)子 context,同時(shí)返回一個(gè)取消函數(shù),用于在需要的時(shí)候取消該 context。與 WithCancel 不同的是,WithDeadline 可以設(shè)置一個(gè)超時(shí)截止日期,當(dāng)截止日期到達(dá)時(shí),子 context 會自動被取消。 -
context.WithTimeout(parent, timeout)WithTimeout 函數(shù)是 WithDeadline 的一個(gè)特例,它也可以派生一個(gè)子 context,并設(shè)置超時(shí)時(shí)間。與 WithDeadline 不同的是,WithTimeout 可以設(shè)置一個(gè)相對于超時(shí)任務(wù),使用 WithTimeout 更為常見。 -
context.WithValue(parent, key, val)WithValue 函數(shù)可以派生一個(gè)子 context,并在其中存儲鍵值對數(shù)據(jù)。該方法不是線程安全的,因此在并發(fā)環(huán)境下使用時(shí)需要注意。
Context 的最佳實(shí)踐
在使用 Context 時(shí),需要遵循以下最佳實(shí)踐:
- 在函數(shù)參數(shù)中添加一個(gè) context 參數(shù),以便于 Goroutine 可以獲取到該 context。
- 如果一個(gè) Goroutine 創(chuàng)建了多個(gè)子 Goroutine,那么應(yīng)該將相同的 context 實(shí)例傳遞給所有子 Goroutine。
- 當(dāng)一個(gè) context 被取消時(shí),它派生的所有子 context 也應(yīng)該被取消。
- 當(dāng)一個(gè) context 被取消時(shí),其關(guān)聯(lián)的資源(如數(shù)據(jù)庫連接、文件描述符等)應(yīng)該被釋放。
- 當(dāng)使用 WithDeadline 和 WithTimeout 時(shí),應(yīng)該考慮到超時(shí)時(shí)間是否合理,過短的超時(shí)時(shí)間會導(dǎo)致任務(wù)失敗,過長的超時(shí)時(shí)間會浪費(fèi)資源。
總結(jié)
在 Goroutine 編程中,Context 是非常重要的概念。它可以用于在不同的 Goroutine 之間傳遞請求特定值、取消信號以及超時(shí)截止日期等數(shù)據(jù),以協(xié)調(diào) Goroutine 之間的操作。Go 語言中常用的 Context 類型有:Background、TODO、WithCancel、WithDeadline、WithTimeout 和 WithValue。在使用 Context 時(shí),需要遵循一些最佳實(shí)踐,以確保程序的正確性和健壯性。
示例 1:使用 WithCancel 實(shí)現(xiàn) Goroutine 的取消
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
default:
fmt.Printf("worker %d is running\n", id)
case <-ctx.Done():
fmt.Printf("worker %d is cancelled\n", id)
return
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 啟動兩個(gè) worker
go worker(ctx, 1)
go worker(ctx, 2)
// 運(yùn)行一段時(shí)間后取消所有 worker
time.Sleep(time.Second * 3)
cancel()
time.Sleep(time.Second)
}
上述代碼中,我們通過使用 WithCancel 派生了一個(gè)新的 context,并將其傳遞給了兩個(gè) Goroutine。在 main 函數(shù)中,我們等待 3 秒鐘后取消了所有的 Goroutine。在 worker 函數(shù)中,我們使用 select 語句來監(jiān)聽 ctx.Done() 信號,如果 ctx 被取消,我們就結(jié)束 Goroutine 的執(zhí)行。
示例 2:使用 WithTimeout 實(shí)現(xiàn)超時(shí)任務(wù)
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
select {
case <-time.After(time.Second * 2):
fmt.Println("worker completed")
case <-ctx.Done():
fmt.Println("worker cancelled")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
go worker(ctx)
select {
case <-ctx.Done():
fmt.Println("main cancelled")
case <-time.After(time.Second * 4):
fmt.Println("main completed")
}
}
上述代碼中,我們使用 WithTimeout 派生了一個(gè)新的 context,并將其傳遞給了一個(gè) Goroutine。在 worker 函數(shù)中,我們使用 select 語句監(jiān)聽兩個(gè) channel,一是通過 time.After 函數(shù)模擬 2 秒鐘的工作,另一個(gè)是 ctx.Done() 信號。如果 ctx 被取消,我們就結(jié)束 Goroutine 的執(zhí)行。在 main 函數(shù)中,我們使用 select 語句監(jiān)聽兩個(gè) channel,一個(gè)是 ctx.Done() 信號,一個(gè)是通過 time.After 函數(shù)模擬 4 秒鐘的執(zhí)行時(shí)間。這樣,如果 worker Goroutine 能在 3 秒鐘之內(nèi)完成工作,程序就會輸出 "main completed",否則程序就會輸出 "main cancelled"。
示例 3:使用 WithValue 存儲請求特定的值
package main
import (
"context"
"fmt"
)
type key int
const nameKey key = 0
func worker(ctx context.Context) {
if name, ok := ctx.Value(nameKey).(string); ok {
fmt.Printf("worker: hello, %s!\n", name)
} else {
fmt.Println("worker: no name found")
}
}
func main() {
ctx := context.WithValue(context.Background(), nameKey, "Alice")
go worker(ctx)
// 等待一段時(shí)間,以便讓 worker 完成工作
fmt.Scanln()
}
上述代碼中,我們使用 WithValue 函數(shù)在 context 中存儲了一個(gè)值。在 worker 函數(shù)中,我們通過 ctx.Value 函數(shù)來獲取這個(gè)值,并將其作為字符串類型打印出來。在 main 函數(shù)中,我們使用 fmt.Scanln 函數(shù)等待用戶的輸入,以便讓程序保持運(yùn)行狀態(tài),直到 worker Goroutine 完成工作。
示例 4:使用 WithDeadline 設(shè)置任務(wù)的截止時(shí)間
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
deadline, ok := ctx.Deadline()
if ok {
fmt.Printf("worker: deadline set to %s\n", deadline.Format(time.RFC3339))
}
select {
case <-time.After(time.Second * 2):
fmt.Println("worker completed")
case <-ctx.Done():
fmt.Println("worker cancelled")
}
}
func main() {
d := time.Now().Add(time.Second * 3)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
go worker(ctx)
select {
case <-ctx.Done():
fmt.Println("main cancelled")
case <-time.After(time.Second * 4):
fmt.Println("main completed")
}
}
上述代碼中,我們使用 WithDeadline 派生了一個(gè)新的 context,并將其傳遞給了一個(gè) Goroutine。在 worker 函數(shù)中,我們使用 ctx.Deadline 函數(shù)獲取任務(wù)的截止時(shí)間,并將其格式化后打印出來。在 select 語句中,我們使用 time.After 函數(shù)模擬了 2 秒鐘的工作,另一個(gè)是 ctx.Done() 信號。如果 ctx 被取消,我們就結(jié)束 Goroutine 的執(zhí)行。在 main 函數(shù)中,我們使用 select 語句監(jiān)聽兩個(gè) channel,一個(gè)是 ctx.Done() 信號,一個(gè)是通過 time.After 函數(shù)模擬 4 秒鐘的執(zhí)行時(shí)間。這樣,如果 worker Goroutine 能在 3 秒鐘之內(nèi)完成工作,程序就會輸出 "main completed",否則程序就會輸出 "main cancelled"。
這些示例代碼演示了不同類型的 Context 的用法,它們都有自己的特點(diǎn)和適用場景。在實(shí)際的開發(fā)過程中,我們需要根據(jù)具體情況來選擇使用哪種類型的 Context,并且在 Goroutine 中使用 Context 時(shí),要遵循一些最佳實(shí)踐,比如:
- 在每個(gè) Goroutine 的入口處創(chuàng)建一個(gè)新的 Context 對象,并將其傳遞給下一級函數(shù)或者 Goroutine;
- 在 Goroutine 中使用 select 語句監(jiān)聽 ctx.Done() 信號,如果收到該信號,應(yīng)該盡快結(jié)束 Goroutine 的執(zhí)行;
- 在使用 Context 時(shí)要注意線程安全性,避免出現(xiàn)競態(tài)條件或者數(shù)據(jù)競爭的情況。
總之,Context 是 Go 語言中非常重要的一個(gè)概念,它可以幫助我們實(shí)現(xiàn) Goroutine 的取消、超時(shí)、請求特定的值等功能,同時(shí)還能避免出現(xiàn) Goroutine 泄漏等問題。因此,在實(shí)際的開發(fā)過程中,我們需要充分了解并熟練掌握 Context 的使用方法,以便在編寫高并發(fā)的應(yīng)用程序時(shí),能夠更好地利用 Goroutine 來提高程序的性能和響應(yīng)速度。
希望本篇文章能夠?qū)δ私夂驼莆?Go 語言中的 Context 有所幫助。如果您有任何疑問或者建議,歡迎在評論區(qū)留言。