在Go编程中,上下文(context
)是一个非常重要的概念,它包含了与请求相关的信息,如截止日期和取消信息,以及在请求处理管道中传递的其他数据。在并发编程中,特别是在处理请求时,正确处理上下文可以确保我们尊重和执行请求中设定的限制,如截止时间。
让我们通过一些代码示例来探讨如何在并发计算中使用上下文,以及如何在处理请求时尊重上下文所设定的截止日期和取消要求。
// download 函数用于下载给定 URL 的内容。 func download(ctx context.Context, url string) (string, error) {...}
download
函数尝试获取给定 URL 的内容。然而,需要注意的是,每个 URL 的下载内容可能不同,因此下载所需的时间也可能不同。如果在截止日期之前未能完成 URL 的下载,该函数将返回一个错误(截止日期错误)。
现在,假设我们需要下载许多 URL,并且我们只有有限的时间来完成这些下载。我们可以使用 errgroup
来并发地进行下载,如果超过截止时间,我们将取消所有并发操作。
// downloadAll 函数并发地下载给定 URL 的内容。 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 }
在这个示例中,downloadAll
函数同时下载每个给定的 URL,并将相同的上下文传递给 download
函数。如果下载任何一个 URL 所需的时间超过了设定的截止时间,download
函数将失败,从而导致整个并发流程也失败,downloadAll
将返回一个截止日期错误。
除了下载这些 URL,我们还需要处理下载的内容。例如,我们可能要对每个 URL 的内容应用某个过滤器(谓词)。
// filter 函数检查给定内容是否符合给定的谓词。 func filter(content string, pred func(string) bool) bool { return pred(content) }
请注意,过滤器既不需要上下文,也不进行任何跨边界调用。过滤器函数不关心上游处理的截止日期。
使用 filter
函数,我们可以定义一个过滤所有内容的函数。
// filterAll 函数同时过滤所有给定的内容。 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
函数调用 filter
函数来应用谓词到每个内容上,但谓词的应用可能会花费一些时间,可能超过上下文设置的截止时间。由于 filter
函数不使用上下文,因此它不会因为截止日期错误而失败。
我们需要重新定义 filterAll
,使其使用上下文并检查其中的错误,而不管 filter
函数是否使用了上下文。
// filterAll 函数同时过滤所有内容,并检查上下文中的错误。 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 }
我们的新实现 filterAll
函数会检查上下文中的任何错误,即使上下文并未直接传递给下游函数(在本例中为 filter
)。如果发生了与上下文相关的截止日期(或任何其他错误),整个过滤过程就会失败。
现在,让我们完成对所有内容的处理。
// processURLs 函数下载每个 URL 的内容并对其进行过滤。 // // 处理必须在上下文截止日期内完成。 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 }
如果任何一个下载操作花费的时间过长,那么在尝试获取内容时就会发生截止日期错误,因为上下文被直接用于 API 调用。因此,downloadAll
函数也会失败,进而导致 processURLs
失败。
如果所有的 URL 在截止日期内都被正确下载,我们将继续对它们进行过滤。在对每个下载内容进行过滤时,不使用上下文,但 filterAll
函数明确地检查上下文中的错误,如果发生了与上下文相关的截止日期(或任何其他错误),整个过滤过程就会失败。
有时候,仅仅使用 errgroup.WithContext
是不足以检测到上下文中的截止日期或其他问题的,特别是当上下文未直接使用时。因此,我们应该定期检查是否仍在时间限制内,否则就会失败。
最后,我们可以通过编写 filterAll
的测试来确保我们正确地处理了类似的情况,以确保我们尊重与上下文相关的任何错误。
func TestContextError(t *testing.T) { ctx, done := context.WithTimeout(context.Background(), time.Nanosecond) defer done() // 生成我们想要应用过滤器的一些数据。 var contents []string = testingContent() _, err := filterAll(ctx, contents, thePredicate) if err == nil { t.Errorf("filterAll() = %v, want error", err) } }
请注意,在测试中,我们期望 filterAll
会失败,因为我们设置的超时时间只有一纳秒。因此,上下文应该因为超过截止时间而发生错误。如果在启动 Goroutine 进行下载内容过滤时不检查 context.Err()
,我们将永远不会处理此类错误。