In this post, we’ll learn about Go’s context package, and more specifically, how we can use it to improve our applications performance.
在本篇文章中,我们将了解 Go 的上下文包,更具体地说,我们如何使用它来提高应用程序的性能。

banner image

The main purpose of using Context is to manage long-running processes (like an HTTP request, a database call, or a network call) in a way that doesn’t waste resources.
使用 Context 的主要目的是以不浪费资源的方式管理长期运行的进程(如 HTTP 请求、数据库调用或网络调用)。

If used correctly, the context package can help you:
如果使用得当,上下文包可以帮助您:

  1. Cancel long running processes that are no longer needed
    取消不再需要的长期运行进程
  2. Pass around request-scoped data and cancellation signals between function calls
    在函数调用之间传递请求作用域数据和取消信号
  3. Set deadlines for processes to complete
    设定完成流程的最后期限

When Do We Use Context?
何时使用语境?

The main use of a context instance is to pass common scoped data within our application. For example:
上下文实例的主要用途是在我们的应用程序中传递共同作用域的数据。例如

  • Request IDs for function calls and goroutines that are part of the same HTTP request
    属于同一 HTTP 请求的函数调用和程序的请求 ID
  • Errors encountered when fetching data from a database
    从数据库获取数据时遇到的错误
  • Cancellation signals created when performing async operations using goroutines
    使用 goroutines 执行异步操作时产生的取消信号

context refers to common scoped data within goroutines or function calls

Using the Context type is the idiomatic way to pass information across these kind of operations, such as:
使用上下文类型是在此类操作中传递信息的惯用方式,例如

  1. Cancellation and deadline signals to terminate the operation
    取消和终止操作的最后期限信号
  2. Miscellaneous data required at every function call invoked by the operation
    操作每次调用函数时都需要的杂项数据

Creating a New Context 创建新背景

We can create a new context using the context.Background() function:
我们可以使用 context.Background() 函数创建一个新的上下文:

ctx := context.Background()

This function returns a new context instance that is empty and has no values associated with it.
该函数返回一个新的上下文实例,该实例为空,且没有相关的值。

In many cases, we won’t be creating a new context instance, but rather using an existing one.
在很多情况下,我们不会创建新的上下文实例,而是使用现有的上下文实例。

For example, when we’re handling an HTTP request, we can use the http.Request.Context() function to get the request’s context:
例如,在处理 HTTP 请求时,我们可以使用 http.Request.Context() 函数来获取请求的上下文:

// Create an HTTP server that listens on port 8000
http.ListenAndServe(":8010", 
  http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Get the request's context
    ctx := r.Context()
    // ...
}))

Creating a Derived Context
创建派生上下文

When you receive a context from an external source, you can add your own values and cancellation signals to it by creating a derived context.
从外部源接收上下文时,可以通过创建派生上下文来添加自己的值和取消信号。

We can do this by using a decorator function like context.WithValue, context.WithCancel, or context.WithDeadline:
我们可以通过使用 context.WithValue、context.WithCancel 或 context.WithDeadline 等装饰器函数来实现这一点:

derived context

Each of these decorators has different effects on the context instance. Let’s take a look at each of them.
每个装饰器对上下文实例都有不同的影响。让我们逐一了解一下。

Context Cancellation Signals
上下文取消信号

One of the most common use cases of the context package is to propagate cancellation signals across function calls.
上下文包最常见的用例之一是在函数调用中传播取消信号。

Why Do We Need Cancellation?
为什么需要取消订单?

In short, we need cancellation to prevent our system from doing unnecessary work.
总之,我们需要取消,以防止我们的系统做不必要的工作。

Consider the common situation of an HTTP server making a call to a database, and returning the queried data to the client:
考虑一下常见的情况:HTTP 服务器向数据库发出调用,并将查询到的数据返回给客户端:

client server model diagram

The timing diagram, if everything worked perfectly, would look like this:
如果一切正常,时序图应该是这样的:

timing diagram with all events finishing

But, what would happen if the client cancelled the request in the middle? This could happen if, for example, the client closed their browser mid-request.
但是,如果客户端中途取消了请求,会发生什么情况呢?例如,如果客户在请求中途关闭浏览器,就可能发生这种情况。

Without cancellation, the application server and database would continue to do their work, even though the result of that work would be wasted:
如果不取消,应用服务器和数据库就会继续工作,尽管工作的结果会被浪费:

timing diagram with http request cancelled, and other processes still taking place

Ideally, we would want all downstream components of a process to halt, if we know that the process (in this example, the HTTP request) halted:
理想情况下,如果我们知道进程(在本例中为 HTTP 请求)停止了,我们就会希望进程的所有下游组件都停止:

timing diagram with all processes cancelling once HTTP request is cancelled

Now that we know why we need cancellation, let’s get into how you can implement it in Go.
既然知道了为什么需要取消,我们就来看看如何在 Go 中实现它。

Because “cancellation” is highly contextual to the operation being performed, the best way to implement it is through context.
由于 "取消 "与正在执行的操作密切相关,因此实现 "取消 "的最佳方法是通过 context 来实现。

There are two sides to context cancellation:
取消语境有两个方面:

  1. Listening for the cancellation signal
    监听取消信号
  2. Emitting the cancellation signal
    发射取消信号

Listening For Cancellation Signals
监听取消信号

The Context type provides a Done() method. This returns a channel that receives an empty struct{} type every time the context receives a cancellation signal.
Context 类型提供了一个 Done() 方法。每次上下文接收到取消信号时,该方法都会返回一个接收空 struct{} 类型的通道。

So, to listen for a cancellation signal, we need to wait on <- ctx.Done().
因此,要监听取消信号,我们需要等待 <- ctx.Done()

For example, lets consider an HTTP server that takes two seconds to process an event. If the request gets cancelled before that, we want to return immediately:
例如,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 piece 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")
		}
	}))
}

You can view the source code for all the examples on Github
您可以在 Github 上查看所有示例的源代码

You can test this by running the server and opening localhost:8000 on your browser. If you close your browser before 2 seconds, you should see “request cancelled” printed on the terminal window.
您可以运行服务器并在浏览器上打开 localhost:8000 进行测试。如果在 2 秒前关闭浏览器,就会在终端窗口中看到 "请求已取消"。

Emitting a Cancellation Signal
发射消除信号

If you have an operation that could be cancelled, you will have to emit a cancellation signal through the context.
如果有可以取消的操作,则必须通过上下文发出取消信号。

This can be done using the WithCancel function in the context package, which returns a context object, and a function.
这可以使用 context 包中的 WithCancel 函数来实现,该函数会返回一个 context 对象和一个函数。

ctx, fn := context.WithCancel(ctx)

This function takes no arguments, and does not return anything, and is called when you want to cancel the context.
该函数不带参数,也不返回任何内容,在要取消上下文时调用。

Consider the case of 2 dependent operations. Here, “dependent” means if one fails, it doesn’t make sense for the other to complete. If we get to know early on that one of the operations failed, we would like to cancel all dependent operations.
考虑 2 个依赖操作的情况。这里的 "依赖 "指的是,如果其中一个操作失败,另一个操作就无法完成。如果我们很早就知道其中一个操作失败了,我们就会取消所有依赖操作。

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)
}

Output (full code): 输出(完整代码):

halted operation2

Cancellation Signals with Causes
带原因的取消信号

In the previous example, calling the cancel() function did not provide any information about why the context was cancelled. There are some cases where you might want to know the reason for cancellation.
在前面的示例中,调用 cancel() 函数并没有提供任何有关上下文被取消的原因的信息。在某些情况下,您可能想知道取消的原因。

For example, consider that you have a long running operation that is dependent on a database call. If the database call fails, you want know that the operation was cancelled because of the database failure, and not because of some other reason.
例如,您有一个依赖于数据库调用的长期运行操作。如果数据库调用失败,您想知道取消操作的原因是数据库故障,而不是其他原因。

In these cases, we can use the context.WithCancelCause instead. This function returns a context object, and a function that takes an error as an argument.
在这种情况下,我们可以使用 context.WithCancelCause 代替。该函数返回一个上下文对象和一个以错误为参数的函数。

Let’s see the same example as before, but with the WithCancelCause function:
让我们看看与之前相同的示例,但使用的是 WithCancelCause 函数:

func operation1(ctx context.Context) error {
	time.Sleep(100 * time.Millisecond)
	return errors.New("failed")
}

func operation2(ctx context.Context) {
	select {
	case <-time.After(500 * time.Millisecond):
		fmt.Println("done")
	case <-ctx.Done():
    // We can get the error from the context
    err := context.Cause(ctx)
		fmt.Println("halted operation2 due to error: ", err)
	}
}

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancelCause(ctx)

	go func() {
		err := operation1(ctx)
		if err != nil {
      // this time, we pass in the error as an argument
			cancel(err)
		}
	}()

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

Output (full code): 输出(完整代码):

halted operation2 due to error:  failed

Let’s summarize the error propagation pattern in this example:
让我们总结一下这个例子中的错误传播模式:

  1. The context.WithCancelCause gives us the cancel function, which we can call with an error.
    context.WithCancelCause 给我们提供了 cancel 函数,我们可以在调用该函数时出错。
  2. Once we encounter and error, we call the cancel function with the error as an argument.
    一旦遇到错误,我们就会调用以错误为参数的 cancel 函数。
  3. Now that the context is cancelled, the ctx.Done() channel will receive a message.
    现在取消了上下文, ctx.Done() 频道将收到一条信息。
  4. We can get the error from the context using the context.Cause function.
    我们可以使用 context.Cause 函数从上下文中获取错误信息。

cancel with cause

Context Deadlines 背景 截止日期

If we want to set a deadline for a process to complete, we should use time based cancellation.
如果我们要为一个流程设定完成的最后期限,就应该使用基于时间的取消。

The functions are almost the same as the previous example, with a few additions:
这些功能与上一个示例几乎相同,只是增加了一些功能:

// 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)

// Setting a context deadline is similar to setting a timeout, except
// you specify a time when you want the context to cancel, rather than a duration.
// Here, 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))

For example, consider making an HTTP API call to an external service. If the service takes too long, it’s better to fail early and cancel the request:
例如,考虑向外部服务调用 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 status code if the request succeeds
	fmt.Println("Response received, status code:", res.StatusCode)
}

Full code 完整代码

Based on how fast the google homepage responds to your request, you will receive:
根据谷歌主页对您请求的响应速度,您将收到以下信息:

Response received, status code: 200

or

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

You can play around with the timeout to achieve both of the above results.
您可以随意调整超时时间,以达到上述两种效果。

Is this code safe to run?
运行这段代码安全吗?
func doSomething() {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	someArg := "loremipsum"
	go doSomethingElse(ctx, someArg)
}

Would it be safe to execute the doSomething function?
执行 doSomething 函数是否安全?

When we execute doSomething, we create a context and defer its cancellation, which means the context will cancel after doSomething finishes execution.

We pass this same context to the doSomethingElse function, which may rely on the context provided to it. Since doSomethingElse is executed in a different goroutine, its likely that the context will cancel before doSomethingElse finishes it’s execution.

doSomething cancelling doSomethingElse

Unless this is explicitly what you want, you should always create a new context when running a function in a different goroutine, like so:

func doSomething() {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	someArg := "loremipsum"
  // create a new context for the goroutine
	go doSomethingElse(context.Background(), someArg)
}

Context Values 背景价值

You can use the context variable to propagate request scoped values that are common across an operation. This is the more idiomatic alternative to just passing them around as arguments throughout your function calls.
您可以使用上下文变量来传播在整个操作中通用的请求作用域值。这比在整个函数调用中将它们作为参数传递更为简便易行。

Some example of values that you might want to propagate using the context variable are:
您可能希望使用上下文变量传播一些值,例如

  1. Request IDs, so that you can trace the execution of a request across multiple function calls.
    请求 ID,以便在多次函数调用中跟踪请求的执行情况。
  2. Errors encountered when fetching data from a database
    从数据库获取数据时遇到的错误
  3. Authentication tokens and user information
    身份验证令牌和用户信息

We can implement the same functionality using the context.WithValue decorator function:
我们可以使用 context.WithValue 装饰器函数实现相同的功能:

// we need to set a key that tells us where the data is stored
const keyID = "id"

func main() {
	// create a request ID as a random number
  rand.Seed(time.Now().Unix())
  requestID := rand.Intn(1000)

  // create a new context variable with a key value pair
	ctx := context.WithValue(context.Background(), keyID, requestID)
	operation1(ctx)
}

func operation1(ctx context.Context) {
	// do some work

	// we can get the value from the context by passing in the key
	log.Println("operation1 for id:", ctx.Value(keyID), " completed")
	operation2(ctx)
}

func operation2(ctx context.Context) {
	// do some work

	// this way, the same ID is passed from one function call to the next
	log.Println("operation2 for id:", ctx.Value(keyID), " completed")
}

Output (full code): 输出(完整代码):

2023/07/05 23:13:50 operation1 for id: 8151872133066627850  completed
2023/07/05 23:13:50 operation2 for id: 8151872133066627850  completed

Here, we’re creating a new context variable in the main function and a key value pair associated with it. The value can then be used by the successive function calls to obtain contextual information.
在这里,我们在 main 函数中创建了一个新的上下文变量,并创建了与之关联的键值对。随后,连续的函数调用将使用该值来获取上下文信息。

main creates a new context which is passed to other functions

Using the context variable to pass down operation-scoped information is useful for a number of reasons:
使用上下文变量传递操作范围内的信息非常有用,原因有很多:

  1. It is thread safe: You can’t modify the value of a context key once it has been set. The only way set another value for a given key is to create another context variable using context.WithValue
    它是线程安全的:一旦设置了上下文键,就不能修改它的值。为给定键设置另一个值的唯一方法是使用 context.WithValue 创建另一个上下文变量。
  2. It is conventional: The context package is used throughout Go’s official libraries and applications to convey operation-scoped data. Other developers and libraries generally play nicely with this pattern.
    它是传统的:整个 Go 的官方库和应用程序都使用上下文包来传递操作范围内的数据。其他开发人员和库通常也会很好地使用这种模式。

Gotchas and Caveats 缺陷和注意事项

Although context cancellation in Go is a versatile tool, there are a few things that you should keep in mind before proceeding. The most important of which, is that a context can only be cancelled once.
尽管 Go 中的上下文取消是一个通用工具,但在继续之前,您还是应该记住一些事情。其中最重要的一点是,上下文只能取消一次。

If there are multiple errors that you would want to propagate in the same operation, then using context cancellation may not the best option.
如果您想在同一操作中传播多个错误,那么使用上下文取消可能不是最佳选择。

The most idiomatic way to use cancellation is when you actually want to cancel something, and not just notify downstream processes that an error has occurred.
使用 "取消 "的最惯用方式是当您确实想要取消某项操作时,而不仅仅是通知下游进程发生了错误。

Another important caveat has to do with wrapping the same context multiple times.
另一个重要的注意事项与多次包装同一上下文有关。

Wrapping an already cancellable context with WithTimeout or WithCancel will enable multiple locations in your code in which your context could be cancelled, and this should be avoided.
使用 WithTimeoutWithCancel 对已经可以取消的上下文进行包装,会使代码中出现多个可以取消上下文的位置,因此应避免这种做法。

The source code for all the above examples can be found on Github
上述所有示例的源代码均可在 Github 上找到