Wednesday, August 23, 2017

Context-Based Programming in Go

Context-Based Programming in Go

Go programs that run multiple concurrent computations in goroutines need to manage their lifetime. Runaway goroutines can get into infinite loops, deadlock other waiting goroutines, or just take too long. Ideally, you should be able to cancel goroutines or have them time out after a fashion. 

Enter content-based programming. Go 1.7 introduced the context package, which provides exactly those capabilities as well as the ability to associate arbitrary values with a context that travels with the execution of requests and allows out-of-band communication and information passing. 

In this tutorial, you'll learn the ins and outs of contexts in Go, when and how to use them, and how to avoid abusing them. 

Who Needs a Context?

The context is a very useful abstraction. It allows you to encapsulate information that is not relevant to the core computation like request id, authorization token, and timeout. There are several benefits of this encapsulation:

  • It separates the core computation parameters from the operational parameters.
  • It codifies common operational aspects and how to communicate them across boundaries.
  • It provides a standard mechanism to add out-of-band information without changing signatures.

The Context Interface

Here is the entire Context interface:

The following sections explain the purpose of each method.

The Deadline() Method

Deadline returns the time when work done on behalf of this context should be canceled. Deadline returns ok==false when no deadline is set. Successive calls to Deadline return the same results.

The Done() Method

Done() returns a channel that's closed when work done on behalf of this context should be canceled. Done may return nil if this context can never be canceled. Successive calls to Done() return the same value.

  • The context.WithCancel() function arranges for the Done channel to be closed when cancel is called. 
  • The context.WithDeadline() function arranges for the Done channel to be closed when the deadline expires.
  • The context.WithTimeout() function arranges for the Done channel to be closed when the timeout elapses.

Done can be used in select statements:

See this article from the Go blog for more examples of how to use a Done channel for cancelation.

The Err() Method

Err() returns nil as long as the Done channel is open. It returns Canceled if the context was canceled or DeadlineExceeded if the context's deadline passed or the timeout expired. After Done is closed, successive calls to Err() return the same value. Here are the definitions:

The Value() Method

Value returns the value associated with this context for a key, or nil if no value is associated with the key. Successive calls to Value with the same key return the same result.

Use context values only for request-scoped data that transitions processes and API boundaries, not for passing optional parameters to functions.

A key identifies a specific value in a Context. Functions that wish to store values in Context typically allocate a key in a global variable and use that key as the argument to context.WithValue() and Context.Value(). A key can be any type that supports equality.

Context Scope

Contexts have scopes. You can derive scopes from other scopes, and the parent scope has no access to values in derived scopes, but derived scopes have access to the parent's scopes values. 

The contexts form a hierarchy. You start with context.Background() or context.TODO(). Whenever you call WithCancel(), WithDeadline(), or WithTimeout(), you create a derived context and receive a cancel func. The important thing is that when a parent context is cancelled or expired, all its derived contexts.

You should use context.Background() in the main() function, init() functions, and tests. You should use context.TODO() if you're not sure what context to use.

Note that Background and TODO are not cancellable.

Deadlines, Timeouts, and Cancellations

As you recall, WithDeadline() and WithTimeout() return contexts that get cancelled automatically, while WithCancel() returns a context and must be cancelled explicitly. All of them return a cancel function, so even if the timeout/deadline didn't expire yet, you may still cancel any derived context. 

Let's examine an example. First, here is the contextDemo() function with a name and a context. It runs in an infinite loop, printing to the console its name and its context's deadline if any. Then it just sleeps for a second.

The main function creates three contexts: 

  • timeoutContext with a three-second timeout
  • a non-expiring cancelContext
  • deadlineContext, which is derived from cancelContext, with a deadline four hours from now

Then, it launches the contextDemo function as three goroutines. All run concurrently and print their message every second. 

The main function then waits for the goroutine with the timeoutCancel to be cancelled by reading from its Done() channel (will block until it's closed). Once the timeout expires after three seconds, main() calls the cancelFunc() that cancels the goroutine with the cancelContext as well as the last goroutine with the derived four hours deadline context.

Here is the output:

Passing Values in the Context

You can attach values to a context using the WithValue() function. Note that the original context is returned, not a derived context. You can read the values from the context using the Value() method. Let's modify our demo function to get its name from the context instead of passing it as a parameter:

And let's modify the main function to attach the name via WithValue():

The output remains the same. See the best practices section for some guidelines about using context values appropriately.

Best Practices

Several best practices have emerged around context values:

  • Avoid passing function arguments in context values.
  • Functions that wish to store values in Context typically allocate a key in a global variable.
  • Packages should define keys as an unexported type to avoid collisions.
  • Packages that define a Context key should provide type-safe accessors for the values stored using that key.

The HTTP Request Context

One of the most useful use cases for contexts is passing information along with an HTTP request. That information may include a request id, authentication credentials, and more. In Go 1.7, the standard net/http package took advantage of the context package getting "standardized" and added context support directly to the request object:

Now it's possible to attach a request id from the headers all the way to the final handler in a standard way. The WithRequestID() handler function extracts a request ID from the "X-Request-ID" header and generates a new context with the request id from an existing context that it uses. It then passes it to the next handler in the chain. The GetRequestID() public function provides access to handlers that may be defined in other packages.

Conclusion

Context-based programming provides a standard and well-supported way to address two common problems: managing the lifetime of goroutines and passing out-of-band information across a chain of functions. 

Follow the best practices and use contexts in the right context (see what I did there?) and your code will improve considerably.


No comments:

Post a Comment