在Go语言中进行并发编程时,我们通常需要协同处理多个任务,其中有些任务可能需要在特定条件下停止或者取消执行。这时候,context包就能够帮我们实现控制并发执行的任务。
1. 理解Context
在介绍如何在Go中使用context包实现并发任务控制前,我们先来了解一下它的基本概念。
一般来说,context是程序执行的上下文环境,包括了一些元数据、系统环境和运行状态等信息。在Go语言中,context包提供了一种方式,使得我们能够在同一个goroutine或者跨goroutine间传递请求作用域中的变量。
context包中最常用的两个类型分别是Context和CancelFunc。前者是实际的上下文环境,而后者是一个取消函数,能够在需要停止某个任务时,向Context发送信号,让它自动终止运行。
2. Context的使用方法
2.1 Context的创建
我们可以使用context包中的WithCancel、WithDeadline、WithTimeout和WithValue等函数创建Context。其中,WithValue函数可以与前三个函数搭配使用,实现跨goroutine传递数据。
2.2 Context的取消
当Context的Done方法返回值为一个已关闭的channel时,表示Context已被取消。此时,我们需要通过Context的Err方法来获取取消原因。同时,我们也可以通过Context的Done方法阻塞,等待Context的取消信号。
3. 在并发任务中使用Context
下面,我们将通过利用Context,来实现一个并发执行多个任务的示例。
假设我们现在需要开发一个服务,用于向多个远程API提交请求,并将它们合并为一个响应。对于每个请求,我们为其封装了一个独立的goroutine:
type Request struct {
Method string
URL string
Payload []byte
}
type Response struct {
StatusCode int
Body []byte
Err error
}
func doRequest(ctx context.Context, req Request) (*Response, error) {
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewBuffer(req.Payload))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
ctxReq := httpReq.WithContext(ctx)
resp, err := http.DefaultClient.Do(ctxReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return &Response{
StatusCode: resp.StatusCode,
Body: body,
Err: nil,
}, nil
}
在这个示例中,我们使用了http.Do函数来执行HTTP请求,同时传入了一个带有Context的httpReq。如果任何一个请求的Context被取消,http.Do函数会自动取消相应的请求。
下面是我们如何使用Context控制并发任务的代码:
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
requests := []Request{
{
Method: "GET",
URL: "https://jsonplaceholder.typicode.com/todos/1",
Payload: nil,
},
{
Method: "GET",
URL: "https://jsonplaceholder.typicode.com/todos/2",
Payload: nil,
},
}
responses := make(chan *Response, len(requests))
for _, r := range requests {
r := r
go func() {
resp, err := doRequest(ctx, r)
if err != nil {
resp = &Response{Err: err}
}
responses <- resp
}()
}
for i := 0; i < len(requests); i++ {
select {
case <-ctx.Done():
fmt.Println("Context cancelled!")
return
case resp := <-responses:
fmt.Printf("Response received: %d\n", resp)
}
}
在这个示例中,我们首先创建了一个带有CancelFunc的Context,然后使用一个goroutine为每个请求启动一个执行线程。在处理每个请求响应时,我们先判断是否有错误,如果没有,就将响应保存在一个responses通道中。在主goroutine中,我们使用select语句等待所有请求完成或者某个Context被取消。
最后,如果我们在20秒钟内未收到所有响应,我们可以使用第二个CancelFunc来通知所有正在执行的goroutine,并让它们自动取消。代码如下:
cancel2 := func() {
for range requests {
cancel()
}
}
time.AfterFunc(20*time.Second, cancel2)
4. 总结
通过这篇文章,我们已经学习了如何在Go语言中使用Context来控制并发任务的执行。我们经常会遇到一些远程请求、数据库查询等复杂操作,这些操作带来的不确定性和异常,可能会破坏整个系统的可用性和稳定性。使用Context,可以让我们更方便地控制这些操作的执行,让我们的程序更加健壮。