Go語(yǔ)言并發(fā)編程基礎(chǔ)上下文概念詳解
前言
相信大家以前在做閱讀理解的時(shí)候,一定有從老師那里學(xué)一個(gè)技巧或者從參考答案看個(gè):結(jié)合上下文。根據(jù)上下文我們能夠找到有助于解題的相關(guān)信息,也能更加了解段落的思想。
在開發(fā)過(guò)程中,也有這個(gè)上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能獲取完整的程序信息。那么什么是程序中的上下文呢?
簡(jiǎn)單來(lái)說(shuō),就是在 API 之間或者函數(shù)調(diào)用之間,除了業(yè)務(wù)參數(shù)信息之外的額外信息。比如,服務(wù)器接收到客戶端的 HTTP 請(qǐng)求之后,可以把客戶端的 IP 地址和端口、客戶端的身份信息、請(qǐng)求接收的時(shí)間、Trace ID 等信息放入到上下文中,這個(gè)上下文可以在后端的方法調(diào)用中傳遞。
1 Go 中的 Context
Golang 的上下文也是應(yīng)用開發(fā)常用的并發(fā)控制工具。同理,上下文可以用于在程序中的 API 層或進(jìn)程之間共享請(qǐng)求范圍的數(shù)據(jù),除此之外,Go 的 Context 庫(kù)還提供取消信號(hào)(Cancel)以及超時(shí)機(jī)制(Timeout)。
Context 又被稱為上下文,與 WaitGroup 不同的是,Context 對(duì)于派生 goroutine 有更強(qiáng)的控制力,可以管理多級(jí)的 goroutine。
但我們?cè)?Go 中創(chuàng)建一個(gè) goroutine 時(shí),如果發(fā)生了一個(gè)錯(cuò)誤,并且這個(gè)錯(cuò)誤永遠(yuǎn)不會(huì)終止,而其他程序會(huì)繼續(xù)進(jìn)行。加入有一個(gè)不被調(diào)用的 goroutine 運(yùn)行無(wú)限循環(huán),如下所示:
package main
import "fmt"
func main() {
dataCom := []string{"alex", "kyrie", "kobe"}
go func(data []string) {
// 模擬大量運(yùn)算的死循環(huán)
}(dataCom)
// 其他代碼正常執(zhí)行
fmt.Println("剩下的代碼執(zhí)行正常邏輯")
}
上面的例子并不完整,dataCom goroutine 可能會(huì)也可能不會(huì)成功處理數(shù)據(jù)。它可能會(huì)進(jìn)入無(wú)限循環(huán)或?qū)е洛e(cuò)誤。我們的其余代碼將不知道發(fā)生了什么。
有多種方法可以解決這個(gè)問(wèn)題。其中之一是使用通道向我們的主線程發(fā)送一個(gè)信號(hào),表明這個(gè) goroutine 花費(fèi)的時(shí)間太長(zhǎng),應(yīng)該取消它。
package main
import (
"fmt"
"time"
)
func main() {
stopChannel := make(chan bool)
dataCom := []string{"alex", "kyrie", "kobe"}
go func(stopChannel chan bool) {
go func(data []string) {
// 大量的計(jì)算
}(dataCom)
for range time.After(2 * time.Second) {
fmt.Println("此操作運(yùn)行時(shí)間過(guò)長(zhǎng),取消中")
stopChannel <- true
}
}(stopChannel)
<-stopChannel
// 其他代碼正常執(zhí)行
fmt.Println("剩下的代碼執(zhí)行正常邏輯")
}
上面的邏輯很簡(jiǎn)單。我們正在使用一個(gè)通道向我們的主線程發(fā)出這個(gè) goroutine 花費(fèi)的時(shí)間太長(zhǎng)的信號(hào)。但是同樣的事情可以用 context 來(lái)完成,這正是 context 包存在的原因。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
dataCom := []string{"alex", "kyrie", "kobe"}
go func() {
go func(data []string) {
// 大量的計(jì)算
}(dataCom)
for range time.After(2 * time.Second) {
fmt.Println("此操作運(yùn)行時(shí)間過(guò)長(zhǎng),取消中")
cancel()
return
}
}()
select {
case <-ctx.Done():
fmt.Println("上下文被取消")
}
}
2 Context 接口
Context 接口定義:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 接口定義了 4 個(gè)方法:
Deadline(): 返回取消此上下文的時(shí)間 deadline(如果有)。如果未設(shè)置 deadline 時(shí),則返回 ok==false,此時(shí) deadline 為一個(gè)初始值的 time.Time 值。后續(xù)每次調(diào)用這個(gè)對(duì)象的 Deadline 方法時(shí),都會(huì)返回和第一次調(diào)用相同的結(jié)果。Done(): 返回一個(gè)用于探測(cè) Context 是否取消的 channel,當(dāng) Context 取消會(huì)自動(dòng)將該 channel 關(guān)閉,如果該 Context 不能被永久取消,該函數(shù)返回 nil。例如context.Background();如果Done被 close,Err 方法會(huì)返回 Done 被 close 的原因。Err(): 該方法會(huì)返回 context 被關(guān)閉的原因,關(guān)閉原因由 context 實(shí)現(xiàn)控制,不需要用戶設(shè)置;如果Done()尚未關(guān)閉,則Err()返回 nilValue(): 在樹狀分布的goroutine之間共享數(shù)據(jù),用 map 鍵值的工作方法,通過(guò) key 值查詢 value。
每次創(chuàng)建新上下文時(shí),都會(huì)得到一個(gè)符合此接口的類型。上下文的真正實(shí)現(xiàn)隱藏在這個(gè)包和這個(gè)接口后面。這些是您可以創(chuàng)建的工廠類型的上下文:
context.TODO
context.Background
context.WithCancel
context.WithValue
context.WithTimeout
context.WithDeadline
3 Context Tree
在實(shí)際實(shí)現(xiàn)中,我們通常使用派生上下文。我們創(chuàng)建一個(gè)父上下文并將其傳遞到一個(gè)層,我們派生一個(gè)新的上下文,它添加一些額外的信息并將其再次傳遞到下一層,依此類推。通過(guò)這種方式,我們創(chuàng)建了一個(gè)從作為父級(jí)的根上下文開始的上下文樹。
這種結(jié)構(gòu)的優(yōu)點(diǎn)是我們可以一次性控制所有上下文的取消。如果根信號(hào)關(guān)閉了上下文,它將在所有派生的上下文中傳播,這些上下文可用于終止所有進(jìn)程,立即釋放所有內(nèi)容。這使得上下文成為并發(fā)編程中非常強(qiáng)大的工具。

4 創(chuàng)建上下文
4.1 上下文創(chuàng)建函數(shù)
我們可以從現(xiàn)有的上下文中創(chuàng)建或派生上下文。頂層(根)上下文是使用 Background 或 TODO 方法創(chuàng)建的,而派生上下文是使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 方法創(chuàng)建的。
所有派生的上下文方法都返回一個(gè)取消函數(shù) CancelFunc,但 WithValue 除外,因?yàn)樗c取消無(wú)關(guān)。調(diào)用 CancelFunc 會(huì)取消子項(xiàng)及其子項(xiàng),刪除父項(xiàng)對(duì)子項(xiàng)的引用,并停止任何關(guān)聯(lián)的計(jì)時(shí)器。調(diào)用 CancelFunc 失敗會(huì)泄漏子項(xiàng)及其子項(xiàng),直到父項(xiàng)被取消或計(jì)時(shí)器觸發(fā)。
- context.Background() ctx Context
此函數(shù)返回一個(gè)空上下文。這通常只應(yīng)在主請(qǐng)求處理程序或頂級(jí)請(qǐng)求處理程序中使用。這可用于為主函數(shù)、初始化、測(cè)試以及后續(xù)層或其他 goroutine 派生上下文的時(shí)候。
ctx, cancel := context.Background()
- context.TODO() ctx Context
此函數(shù)返回一個(gè)非 nil 的、空的上下文。沒(méi)有任何值、不會(huì)被 cancel,不會(huì)超時(shí),也沒(méi)有截止日期。但是,這也應(yīng)該僅在您不確定要使用什么上下文或者該函數(shù)還不能用于接收上下文時(shí),可以使用這個(gè)方法,并且將在將來(lái)需要添加時(shí)使用。
ctx, cancel := context.TODO()
- context.WithValue(parent Context, key, val interface{}) Context
這個(gè)函數(shù)接受一個(gè)上下文并返回一個(gè)派生的上下文,其中值 val 與 key 相關(guān)聯(lián),并與上下文一起經(jīng)過(guò)上下文樹。
WithValue 方法其實(shí)是創(chuàng)建了一個(gè)類型為 valueCtx 的上下文,它的類型定義如下:
type valueCtx struct {
Context
key, val interface{}
}
這意味著一旦你得到一個(gè)帶有值的上下文,任何從它派生的上下文都會(huì)得到這個(gè)值。該值是不可變的,因此是線程安全的。
提供的鍵必須是可比較的,并且不應(yīng)該是字符串類型或任何其他內(nèi)置類型,以避免使用上下文的包之間發(fā)生沖突。 WithValue 的用戶應(yīng)該為鍵定義自己的類型。
為避免在分配給 interface{} 時(shí)進(jìn)行分配,上下文鍵通常具有具體類型 struct{}。或者,導(dǎo)出的上下文鍵變量的靜態(tài)類型應(yīng)該是指針或接口。
package main
import (
"context"
"fmt"
)
type contextKey string
func main() {
var authToken contextKey = "auth_token"
ctx := context.WithValue(context.Background(), authToken, "Hello123456")
fmt.Println(ctx.Value(authToken))
}
運(yùn)行該代碼:
$ go run .
Hello123456
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
此函數(shù)接收父上下文并返回派生上下文,返回 parent 的副本,只是副本中的 Done Channel 是新建的對(duì)象,它的類型是 cancelCtx。在這個(gè)派生上下文中,添加了一個(gè)新的 Done channel,該 channel 在調(diào)用 cancel 函數(shù)或父上下文的 Done 通道關(guān)閉時(shí)關(guān)閉。
要記住的一件事是,我們永遠(yuǎn)不應(yīng)該在不同的函數(shù)或?qū)又g傳遞這個(gè) cancel ,因?yàn)樗赡軙?huì)導(dǎo)致意想不到的結(jié)果。創(chuàng)建派生上下文的函數(shù)應(yīng)該只調(diào)用取消函數(shù)。
下面是一個(gè)使用 Done 通道演示 goroutine 泄漏的示例:
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for char := range randomCharGenerator(ctx) {
generatedChar := string(char)
fmt.Printf("%v\n", generatedChar)
if generatedChar == "o" {
break
}
}
}
func randomCharGenerator(ctx context.Context) <-chan int {
char := make(chan int)
seedChar := int('a')
go func() {
for {
select {
case <-ctx.Done():
fmt.Printf("found %v", seedChar)
return
case char <- seedChar:
seedChar = 'a' + rand.Intn(26)
}
}
}()
return char
}
運(yùn)行結(jié)果:
$ go run .
a
m
q
c
l
t
o
- func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
此函數(shù)從其父級(jí)返回派生上下文,返回一個(gè) parent 的副本。
當(dāng)期限超過(guò)或調(diào)用取消函數(shù)時(shí),該派生上下文將被取消。例如,您可以創(chuàng)建一個(gè)在未來(lái)某個(gè)時(shí)間自動(dòng)取消的上下文,并將其傳遞給子函數(shù)。當(dāng)該上下文由于截止日期用完而被取消時(shí),所有獲得該上下文的函數(shù)都會(huì)收到通知停止工作并返回。如果 parent 的截止日期已經(jīng)早于 d,則上下文的 Done 通道已經(jīng)關(guān)閉。
下面是我們正在讀取一個(gè)大文件的示例,該文件的截止時(shí)間為當(dāng)前時(shí)間 2 毫秒。我們將獲得 2 毫秒的輸出,然后將關(guān)閉上下文并退出程序。
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"time"
)
func main() {
// context with deadline after 2 millisecond
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Millisecond))
defer cancel()
lineRead := make(chan string)
var fileName = "sample-file.txt"
file, err := os.Open(fileName)
if err != nil {
log.Fatalf("failed opening file: %s", err)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
// goroutine to read file line by line and passing to channel to print
go func() {
for scanner.Scan() {
lineRead <- scanner.Text()
}
close(lineRead)
file.Close()
}()
outer:
for {
// printing file line by line until deadline is reached
select {
case <-ctx.Done():
fmt.Println("process stopped. reason: ", ctx.Err())
break outer
case line := <-lineRead:
fmt.Println(line)
}
}
}
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
這個(gè)函數(shù)類似于 context.WithDeadline。不同之處在于它將持續(xù)時(shí)間作為輸入而不是時(shí)間對(duì)象。此函數(shù)返回一個(gè)派生上下文,如果調(diào)用取消函數(shù)或超過(guò)超時(shí)持續(xù)時(shí)間,該上下文將被取消。
WithTimeout 的實(shí)現(xiàn)是:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
// 當(dāng)前時(shí)間+timeout就是deadline
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout)) 。
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"time"
)
func main() {
// context with deadline after 2 millisecond
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
lineRead := make(chan string)
var fileName = "sample-file.txt"
file, err := os.Open(fileName)
if err != nil {
log.Fatalf("failed opening file: %s", err)
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
// goroutine to read file line by line and passing to channel to print
go func() {
for scanner.Scan() {
lineRead <- scanner.Text()
}
close(lineRead)
file.Close()
}()
outer:
for {
// printing file line by line until deadline is reached
select {
case <-ctx.Done():
fmt.Println("process stopped. reason: ", ctx.Err())
break outer
case line := <-lineRead:
fmt.Println(line)
}
}
}
如果父上下文的 Done 通道關(guān)閉,它最終將關(guān)閉所有派生的 Done 通道(所有后代),如:
package main
import (
"context"
"fmt"
"time"
)
func main() {
c := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c <- "one"
}()
ctx1 := context.Context(context.Background())
ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
ctx3, cancel3 := context.WithTimeout(ctx2, 10*time.Second) // derives from ctx2
ctx4, cancel4 := context.WithTimeout(ctx2, 3*time.Second) // derives from ctx2
ctx5, cancel5 := context.WithTimeout(ctx4, 5*time.Second) // derives from ctx4
cancel2()
defer cancel3()
defer cancel4()
defer cancel5()
select {
case <-ctx3.Done():
fmt.Println("ctx3 closed! error: ", ctx3.Err())
case <-ctx4.Done():
fmt.Println("ctx4 closed! error: ", ctx4.Err())
case <-ctx5.Done():
fmt.Println("ctx5 closed! error: ", ctx5.Err())
case msg := <-c:
fmt.Println("received", msg)
}
}
在這里,由于我們?cè)趧?chuàng)建其他派生上下文后立即關(guān)閉 ctx2,因此所有其他上下文也會(huì)立即關(guān)閉,隨機(jī)打印 ctx3、ctx4 和 ctx5 關(guān)閉消息。 ctx5 是從 ctx4 派生的,由于 ctx2 關(guān)閉的級(jí)聯(lián)效應(yīng),它正在關(guān)閉。嘗試多次運(yùn)行,您會(huì)看到不同的結(jié)果。
使用 Background 或 TODO 方法創(chuàng)建的上下文沒(méi)有取消、值或截止日期。
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
_, ok := ctx.Deadline()
if !ok {
fmt.Println("no dealine is set")
}
done := ctx.Done()
if done == nil {
fmt.Println("channel is nil")
}
}
4.2 Context 使用規(guī)范
- 不要將上下文存儲(chǔ)在結(jié)構(gòu)類型中;相反,將 Context 顯式傳遞給需要它的每個(gè)函數(shù)。 Context 應(yīng)該是第一個(gè)參數(shù),通常命名為 ctx。
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
- 不要傳遞 nil 上下文,即使函數(shù)允許。如果不確定要使用哪個(gè) Context,請(qǐng)傳遞
context.TODO或使用context.Background()創(chuàng)建一個(gè)空的上下文對(duì)象。 - 僅使用上下文傳遞請(qǐng)求范圍的數(shù)據(jù)。不要傳遞應(yīng)該使用函數(shù)參數(shù)傳遞的數(shù)據(jù)。
- 始終尋找 goroutine 泄漏并有效地使用上下文來(lái)避免這種情況。
- 如果父上下文的 Done 通道關(guān)閉,它最終將關(guān)閉所有派生的 Done 通道(所有后代)
- 上下文只是臨時(shí)做函數(shù)之間的上下文傳透,不能持久化上下文
- key 的類型不應(yīng)該是字符串類型或者其它內(nèi)建類型,否則容易在包之間使用 Context 時(shí)候產(chǎn)生沖突。使用 WithValue 時(shí),key 的類型應(yīng)該是自己定義的類型。
4.3 Context 使用場(chǎng)景
- 上下文信息傳遞 (request-scoped),比如處理 http 請(qǐng)求、在請(qǐng)求處理鏈路上傳遞信息;
- 控制子 goroutine 的運(yùn)行;
- 超時(shí)控制的方法調(diào)用;
- 可以取消的方法調(diào)用。
5 總結(jié)
Context 是在 Go 中進(jìn)行并發(fā)編程時(shí)最重要的工具之一。上下文的主要作用是在多個(gè) Goroutine 或者模塊之間同步取消信號(hào)或者截止日期,用于減少對(duì)資源的消耗和長(zhǎng)時(shí)間占用,避免資源浪費(fèi)。標(biāo)準(zhǔn)庫(kù)中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。
參考鏈接:
Go Concurrency Patterns: Context
以上就是Go語(yǔ)言并發(fā)編程基礎(chǔ)上下文概念詳解的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言并發(fā)上下文的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
加速開發(fā):使用Go語(yǔ)言和Gin框架構(gòu)建Web項(xiàng)目的利器
Go語(yǔ)言和Gin框架是構(gòu)建高性能Web項(xiàng)目的利器,Go語(yǔ)言的簡(jiǎn)潔性和并發(fā)性,以及Gin框架的輕量級(jí)和快速路由能力,使開發(fā)者能夠快速構(gòu)建可靠的Web應(yīng)用程序,需要的朋友可以參考下2023-09-09
golang gorm 結(jié)構(gòu)體的表字段缺省值設(shè)置方式
這篇文章主要介紹了golang gorm 結(jié)構(gòu)體的表字段缺省值設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
Golang中switch語(yǔ)句和select語(yǔ)句的用法教程
這篇文章主要給大家介紹了關(guān)于Golang中switch和select的用法教程,文中通過(guò)示例代碼將switch語(yǔ)句與select語(yǔ)句的使用方法介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編一起來(lái)學(xué)習(xí)學(xué)習(xí)吧。2017-06-06
利用go語(yǔ)言實(shí)現(xiàn)查找二叉樹中的最大寬度
這篇文章主要介紹了利用go語(yǔ)言實(shí)現(xiàn)查找二叉樹中的最大寬度,文章圍繞主題展開詳細(xì)介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-05-05
golang構(gòu)建工具M(jìn)akefile使用詳解
這篇文章主要為大家介紹了golang構(gòu)建工具M(jìn)akefile的使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

