Go 利用上下文進(jìn)行并發(fā)計(jì)算,你學(xué)會(huì)了嗎?
在Go編程中,上下文(context)是一個(gè)非常重要的概念,它包含了與請(qǐng)求相關(guān)的信息,如截止日期和取消信息,以及在請(qǐng)求處理管道中傳遞的其他數(shù)據(jù)。在并發(fā)編程中,特別是在處理請(qǐng)求時(shí),正確處理上下文可以確保我們尊重和執(zhí)行請(qǐng)求中設(shè)定的限制,如截止時(shí)間。
讓我們通過(guò)一些代碼示例來(lái)探討如何在并發(fā)計(jì)算中使用上下文,以及如何在處理請(qǐng)求時(shí)尊重上下文所設(shè)定的截止日期和取消要求。
// download 函數(shù)用于下載給定 URL 的內(nèi)容。
func download(ctx context.Context, url string) (string, error) {...}
download 函數(shù)嘗試獲取給定 URL 的內(nèi)容。然而,需要注意的是,每個(gè) URL 的下載內(nèi)容可能不同,因此下載所需的時(shí)間也可能不同。如果在截止日期之前未能完成 URL 的下載,該函數(shù)將返回一個(gè)錯(cuò)誤(截止日期錯(cuò)誤)。
現(xiàn)在,假設(shè)我們需要下載許多 URL,并且我們只有有限的時(shí)間來(lái)完成這些下載。我們可以使用 errgroup 來(lái)并發(fā)地進(jìn)行下載,如果超過(guò)截止時(shí)間,我們將取消所有并發(fā)操作。
// downloadAll 函數(shù)并發(fā)地下載給定 URL 的內(nèi)容。
func downloadAll(ctx context.Context, urls []string) ([]string, error) {
results := make([]string, len(urls))
g, ctx := errgroup.WithContext(ctx)
for i := range len(urls) {
g.Go(func() error {
content, err := download(ctx, urls[i])
if err != nil {
return err
}
results[i] = content
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
在這個(gè)示例中,downloadAll 函數(shù)同時(shí)下載每個(gè)給定的 URL,并將相同的上下文傳遞給 download 函數(shù)。如果下載任何一個(gè) URL 所需的時(shí)間超過(guò)了設(shè)定的截止時(shí)間,download 函數(shù)將失敗,從而導(dǎo)致整個(gè)并發(fā)流程也失敗,downloadAll 將返回一個(gè)截止日期錯(cuò)誤。
除了下載這些 URL,我們還需要處理下載的內(nèi)容。例如,我們可能要對(duì)每個(gè) URL 的內(nèi)容應(yīng)用某個(gè)過(guò)濾器(謂詞)。
// filter 函數(shù)檢查給定內(nèi)容是否符合給定的謂詞。
func filter(content string, pred func(string) bool) bool {
return pred(content)
}
請(qǐng)注意,過(guò)濾器既不需要上下文,也不進(jìn)行任何跨邊界調(diào)用。過(guò)濾器函數(shù)不關(guān)心上游處理的截止日期。
使用 filter 函數(shù),我們可以定義一個(gè)過(guò)濾所有內(nèi)容的函數(shù)。
// filterAll 函數(shù)同時(shí)過(guò)濾所有給定的內(nèi)容。
func filterAll(contents []string, pred func(string) bool) []string {
type Result struct {
content string
ok bool
}
results := make([]Result, len(contents))
g := errgroup.Group{}
for i, content := range contents {
g.Go(func() error {
ok := filter(contents[i], pred)
results[i] = Result{content: content, ok: ok}
return nil
})
}
g.Wait()
var filtered []string
for _, r := range results {
if r.ok {
filtered = append(filtered, r.content)
}
}
return filtered
}
filterAll 函數(shù)調(diào)用 filter 函數(shù)來(lái)應(yīng)用謂詞到每個(gè)內(nèi)容上,但謂詞的應(yīng)用可能會(huì)花費(fèi)一些時(shí)間,可能超過(guò)上下文設(shè)置的截止時(shí)間。由于 filter 函數(shù)不使用上下文,因此它不會(huì)因?yàn)榻刂谷掌阱e(cuò)誤而失敗。
我們需要重新定義 filterAll,使其使用上下文并檢查其中的錯(cuò)誤,而不管 filter 函數(shù)是否使用了上下文。
// filterAll 函數(shù)同時(shí)過(guò)濾所有內(nèi)容,并檢查上下文中的錯(cuò)誤。
func filterAll(ctx context.Context, contents []string, pred func(string) bool) ([]string, error) {
type Result struct {
content string
ok bool
}
results := make([]Result, len(contents))
g, ctx := errgroup.WithContext(ctx)
for i, content := range contents {
g.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
ok := filter(contents[i], pred)
results[i] = Result{content: content, ok: ok}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
var filtered []string
for _, r := range results {
if r.ok {
filtered = append(filtered, r.content)
}
}
return filtered, nil
}
我們的新實(shí)現(xiàn) filterAll 函數(shù)會(huì)檢查上下文中的任何錯(cuò)誤,即使上下文并未直接傳遞給下游函數(shù)(在本例中為 filter)。如果發(fā)生了與上下文相關(guān)的截止日期(或任何其他錯(cuò)誤),整個(gè)過(guò)濾過(guò)程就會(huì)失敗。
現(xiàn)在,讓我們完成對(duì)所有內(nèi)容的處理。
// processURLs 函數(shù)下載每個(gè) URL 的內(nèi)容并對(duì)其進(jìn)行過(guò)濾。
//
// 處理必須在上下文截止日期內(nèi)完成。
func processURLs(ctx context.Context, urls []string) ([]string, error) {
contents, err := downloadAll(ctx, urls)
if err != nil {
return nil, err
}
filtered, err := filterAll(ctx, contents, somePredicate)
return filtered, err
}
如果任何一個(gè)下載操作花費(fèi)的時(shí)間過(guò)長(zhǎng),那么在嘗試獲取內(nèi)容時(shí)就會(huì)發(fā)生截止日期錯(cuò)誤,因?yàn)樯舷挛谋恢苯佑糜?API 調(diào)用。因此,downloadAll 函數(shù)也會(huì)失敗,進(jìn)而導(dǎo)致 processURLs 失敗。
如果所有的 URL 在截止日期內(nèi)都被正確下載,我們將繼
續(xù)對(duì)它們進(jìn)行過(guò)濾。在對(duì)每個(gè)下載內(nèi)容進(jìn)行過(guò)濾時(shí),不使用上下文,但 filterAll 函數(shù)明確地檢查上下文中的錯(cuò)誤,如果發(fā)生了與上下文相關(guān)的截止日期(或任何其他錯(cuò)誤),整個(gè)過(guò)濾過(guò)程就會(huì)失敗。
有時(shí)候,僅僅使用 errgroup.WithContext 是不足以檢測(cè)到上下文中的截止日期或其他問(wèn)題的,特別是當(dāng)上下文未直接使用時(shí)。因此,我們應(yīng)該定期檢查是否仍在時(shí)間限制內(nèi),否則就會(huì)失敗。
最后,我們可以通過(guò)編寫(xiě) filterAll 的測(cè)試來(lái)確保我們正確地處理了類(lèi)似的情況,以確保我們尊重與上下文相關(guān)的任何錯(cuò)誤。
func TestContextError(t *testing.T) {
ctx, done := context.WithTimeout(context.Background(), time.Nanosecond)
defer done()
// 生成我們想要應(yīng)用過(guò)濾器的一些數(shù)據(jù)。
var contents []string = testingContent()
_, err := filterAll(ctx, contents, thePredicate)
if err == nil {
t.Errorf("filterAll() = %v, want error", err)
}
}
請(qǐng)注意,在測(cè)試中,我們期望 filterAll 會(huì)失敗,因?yàn)槲覀冊(cè)O(shè)置的超時(shí)時(shí)間只有一納秒。因此,上下文應(yīng)該因?yàn)槌^(guò)截止時(shí)間而發(fā)生錯(cuò)誤。如果在啟動(dòng) Goroutine 進(jìn)行下載內(nèi)容過(guò)濾時(shí)不檢查 context.Err(),我們將永遠(yuǎn)不會(huì)處理此類(lèi)錯(cuò)誤。