Contexts in Go
If you take a look at the SDK's src directory, among the list of packages, 'context' stands out as the most ambiguous name. The 'context' package migrated to Go 1.7 from golang.org/x/net/context (note the path: 'net' is not accidental; this package was used for canceling HTTP requests). Brad Fitzpatrick, a Go developer from 2010 to 2020, suggested in his proposal the potential use of context not only for canceling HTTP requests but also for stopping TCP dialers (establishing TCP connections), halting OS child processes launched through os/exec, canceling SQL queries, and more. When you encounter a function or method that takes a context.Context, you should perceive it as an opportunity to control the cancellation of its execution.
How can you achieve the task of cancellation without the context package? One of the most obvious ways to implement cancellation is by using a 'done' channel as the first argument to the function. Excellent examples can also be found in the article https://go.dev/blog/pipelines Run in Go Playground
And here's the implementation of timeout-based cancellation: Run in Go Playground
To address the task of procedure cancellation in Go, the SDK provides the 'context' package. The 'context' package is a micro-framework designed for communicating with procedures, often through signals to cancel their execution.
I couldn't find references to the specific choice of this name for the packages. By examining the first publication about the Context Object pattern (Context Object, A Design Pattern for Efficient Information Sharing across Multiple System Layers, 2005), the reasons for such a package name become clear. Quote from the publication:
This pattern provides an efficient, and application transparent way of sharing information between different layers in a software system.The 'context' package in Go also serves for passing information between layers of an application, but in Go, the emphasis is more on information related to the cancellation of procedure execution. The task of communication with a procedure can also be solved by using a different approach, through a manager (Peter Sommerlad, “The Manager Design Pattern," Pattern Languages of Program Design 3, Addison-Wesley, 1998.), but this solution has a significant drawback: concurrent work with the manager requires synchronization between goroutines, for example, through Mutexes.
A bit about how context works in Go. The 'context' package provides the 'Context' interface, along with several implementations, which can be composed into a chain. This is convenient if you have multiple reasons to cancel a function (cancel call, timeout, deadline), and the cancellation should occur when any of them happen. Any user-defined context is a child of another context, which is called the parent. Only the empty context (context.Background() or context.TODO()) has no parent. This hierarchy is important because it determines which of the context chain will be canceled when the parent is canceled. Run in Go Playground
Cancellation of the parent context leads to the cancellation of all child contexts. Run in Go Playground
Cancellation of a child context does not cancel the parent. Run in Go Playground
It is essential to understand that the context itself does not cancel the execution of a function. It merely provides a mechanism for cancellation that needs to be implemented independently. Therefore, it is crucial to inspect the source code of external packages and check how context is actually used. Run in Go Playground
"go vet" will complain if the context cancellation function is not called. Run in Go Playground
But what will happen if the context cancellation function is not called? In the case of timerCtx, a timer is started, and when the time expires, a goroutine is launched, which then calls the cancel function. Only after that, garbage collection occurs (i.e., resource cleanup). In the case of cancelCtx, calling the cancel function clears the map of child contexts. If you are using custom contexts, you may encounter goroutine leaks because only custom contexts initiate a goroutine waiting for the cancellation of a parent or child context. Run in Go Playground
If you plan to use the context solely for passing data, it is better to create a specialized structure for this purpose. Otherwise, you lose all the benefits of static typing since values in the context are stored as 'any' type. The following recommendation may be useful: use context solely for cancellation and store values in the context only when it is justified.
How are values stored in cancelCtx? Each subsequent context holds a reference to the previous context. The cancel context (context.cancelCtx) used under the hood as a context for cancellation by deadline/timeout (timerCtx) holds references to all child contexts to be able to cancel them when the parent context is canceled. Run in Go Playground
Let's consider the usage of context in an HTTP server from the net/http package. The context here serves for passing the HTTP server itself and the TCP address on which it is running. You can also enrich the context with additional data using your own middlewares. Since the base context of the server (Server.BaseContext) passed to each request can be overridden (default is context.Background()), you can use the context, for example, to signal an imminent server restart (see a ready-made implementation in the SDK signal.NotifyContext). Run in Go Playground
In addition to middlewares, you can augment the context with data related to newly established TCP connections through the ConnContext function of the server. Run in Go Playground
Some packages provide ready-made solutions for timeout cancellation. For example, you can use http.TimeoutHandler if you need to limit the response time of a handler. The handler itself will continue its execution until the program finishes if it does not explicitly check whether the request context is canceled. Run in Go Playground
A few concluding recommendations:
- Ensure that you cannot solve the task without using goroutines. If you can do without goroutines, you can do without the context package.
- If you do not plan to cancel a procedure, pass an empty context (context.Background() or context.TODO()).
- If an external function takes a context.Context, check how it is used by inspecting its source code. The context package provides mechanisms for cancellation, but not the cancellation of the procedure itself.