via:
https://www.sohamkamani.com/golang/2018-06-17-golang-using-context-cancellation/
作者:Soham Kamani

使用过 Go 语言的人对 context 包应该都不陌生。context 包经常用于需要执行一些下游操作的地方,比如:执行 HTTP 请求、从数据库获取数据或者使用协程执行异步操作。最普通的用法就是向下游传递一些公共数据。然而,一个 context 鲜为人知但是非常有用的功能就是可以用于中途取消或者停止操作。

接下来的内容我们将会讨论如何使用 context 提供的取消功能,并提供一些最佳实践供参考,为你编写效率更高、代码更健壮的程序提供借鉴。

为什么我们需要取消?

简而言之,取消是为了防止系统做一些不必要的工作。

我们拿一种常见的场景举例,比如:用户发出 HTTP 请求,从数据获取数据并将数据返回给客户端。

11

如果一切正常的话,时序图应该是下面这样的:

2

但是,如果客户端中途取消了请求会发生什么?类似的场景,比如:关闭了浏览器等。如果不取消操作,服务器和数据库仍然会继续完成执行,即使它们的执行成果会被浪费:

3

理想情况下,如果我们知道请求中断了,我们希望该请求下游的所有工作组件都停止执行。

4

Context cancellation in Go

现在我们已经知道为什么需要取消,接着就来看看如何实现。因为取消事件与事务和操作高度相关,所以很自然将它与 context 联系在一起。

取消主要有两个方面:

  1. 监听取消事件;
  2. 发出取消事件;

监听取消事件

Context 类型提供了 Done() 方法,每次 context 接收到取消事件时,该方法都是返回一个 channel,这个 channel 会收到空结构体类型的数据。监听取消事件也很容易,<- ctx.Done()。

比如,一个 HTTP 请求处理需要两秒,如果在中途取消就必须立即返回。

func main() {
	// Create an HTTP server that listens on port 8000
	http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		// This prints to STDOUT to show that processing has started
		fmt.Fprint(os.Stdout, "processing request\n")
		// We use `select` to execute a peice of code depending on which
		// channel receives a message first
		select {
		case <-time.After(2 * time.Second):
			// If we receive a message after 2 seconds
			// that means the request has been processed
			// We then write this as the response
			w.Write([]byte("request processed"))
		case <-ctx.Done():
			// If the request gets cancelled, log it
			// to STDERR
			fmt.Fprint(os.Stderr, "request cancelled\n")
		}
	}))
}

ps:文末有完整代码地址。

你可以使用 go run 将服务跑起来,在浏览器中打开 localhost:8000,如果你在 2s 钟之内关闭浏览器,终端将会输出 request cancelled。

发出取消事件

如果你有需要取消的操作,可以通过 context 发出取消事件。可以通过 context 包提供的 WithCancel 函数完成,该函数返回 context 对象和一个取消函数,这个函数不带任何参数、没有返回值,当你需要取消 context 时可以调用该函数。

假设有两个相互依赖的操作,这里“依赖”的意思是,如果其中一个操作失败了,另一条操作即使完成也没有任何意义。这个场景里,如果我们事先知道其中一个操作失败了,我们需要取消所有的操作。

func operation1(ctx context.Context) error {
	// Let's assume that this operation failed for some reason
	// We use time.Sleep to simulate a resource intensive operation
	time.Sleep(100 * time.Millisecond)
	return errors.New("failed")
}

func operation2(ctx context.Context) {
	// We use a similar pattern to the HTTP server
	// that we saw in the earlier example
	select {
	case <-time.After(500 * time.Millisecond):
		fmt.Println("done")
	case <-ctx.Done():
		fmt.Println("halted operation2")
	}
}

func main() {
	// Create a new context
	ctx := context.Background()
	// Create a new context, with its cancellation function
	// from the original context
	ctx, cancel := context.WithCancel(ctx)

	// Run two operations: one in a different go routine
	go func() {
		err := operation1(ctx)
		// If this operation returns an error
		// cancel all operations using this context
		if err != nil {
			cancel()
		}
	}()

	// Run operation2 with the same context we use for operation1
	operation2(ctx)
}

基于时间的取消事件

任何应用程序都需要在超时时间之内维护 SLA 可用性,可以采用基于时间的取消事件。相关的 API 与上面提到的例子类似,但是有一点补充:

// The context will be cancelled after 3 seconds
// If it needs to be cancelled earlier, the `cancel` function can
// be used, like before
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)

// The context will be cancelled on 2009-11-10 23:00:00
ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))

例如,使用 HTTP API 调用外部服务,如果请求时间太长,最好尽早取消请求。

func main() {
	// Create a new context
	// With a deadline of 100 milliseconds
	ctx := context.Background()
	ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)

	// Make a request, that will call the google homepage
	req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
	// Associate the cancellable context we just created to the request
	req = req.WithContext(ctx)

	// Create a new HTTP client and execute the request
	client := &http.Client{}
	res, err := client.Do(req)
	// If the request failed, log to STDOUT
	if err != nil {
		fmt.Println("Request failed:", err)
		return
	}
	// Print the statuscode if the request succeeds
	fmt.Println("Response received, status code:", res.StatusCode)
}

输出的结果取决于请求谷歌主页的快慢,有可能输出:

Response received, status code: 200

或者

Request failed: Get http://google.com: context deadline exceeded

陷阱和警告

尽管 Context 的取消功能是一个很好用的工具,但是使用时有一些需要主要的点。最重要的是,context 只能被取消一次。如果你希望在同一操作中传递多个错误,那么使用 context 取消可能不是最佳选择。使用取消最常见的场景是仅仅希望取消操作,而不是返回下游操作出现的错误。

需要注意的另一点就是,应将相同的 context 对象传递给可能要取消的所有函数或者协程,使用 WithTimeout 或 WithCancel 包装一个已经可取消的 context 将导致多种可能的上下文被取消,应该避免。

ps: 文章的完整代码



(全文完)

扫码关注领取学习资料!