String context keys

By now we’ve looked at empty structs (struct{}), and integers as possible context key types. Today, let’s consider string context keys. type contextKey string const ( KeyUserID contextKey = "user_id" KeyTransactionID contextKey = "transaction_id" KeyMessageID contextKey = "message_id" ) How does this stack up against our 5 criteria? Must be comparable. ✅ Check. Use minimal memory. ✅ Using a string will typically use a bit more memory than an integer (typically 32 bytes vs 16), but still quite minimal.


Integer context keys

We’re looking at different types for context keys. So far, we’ve looked at empty structs (struct{}), and found it to be less than ideal. Today, let’s consider integer context keys. This seems handy, right? type contextKey int const ( KeyUserID int = iota KeyTransactionID KeyMessageID . . . KeyFoo ) Let’s see how it stacks up to our 5 criteria: Must be comparable. ✅ No problem! Use minimal membory. ✅ int doesn’t have as small a memory footprint as struct{}’s zero bytes, but it’s still pretty small.


Context key types

So we’ve established that context keys should be of an unexported type in your package. That still leaves a lot of options. Which type is ideal? Today we’ll look at some options, and their pros and cons. Before looking at specific types, let’s consider what sorts of things we want from such a type. It must be comparable, or it won’t work as a key. This means we can’t use function or channel types, for example.


Unexported context key types

Last week we saw the potential dangers of context key collisions. Today let’s look at how to avoid this problem. The GoDoc we looked at last week gave us a clue: // packages should define keys as an unexported type to avoid // collisions. Let’s dive into what this is suggesting, and why it works. First: context.WithValue accepts any comparable type as a key. This includes custom types. And by using a custom type that’s unexported, we can ensure that no other package uses our keys.


Context value key collisions

Let’s talk about keys for context values. type Context type Context interface { … // 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 then use that key as the argument to context.WithValue and // Context.Value. A key can be any type that supports equality; // packages should define keys as an unexported type to avoid // collisions.


Context values

I’ve already talked a bit about context values, and when not to use them. For a recap, take a look at these previous posts, so I don’t have to rehash those points today: Context abuse Context values and type safety When not to use context values So from here on out, we’re assuming that you have a legitimate reason to store values in a context. How can/should you go about it?


Context errors

type Context type Context interface { … // If Done is not yet closed, Err returns nil. // If Done is closed, Err returns a non-nil error explaining why: // DeadlineExceeded if the context's deadline passed, // or Canceled if the context was canceled for some other reason. // After Err returns a non-nil error, successive calls to Err return the same error. Err() error Done() (covered yesterday) and Err() (today) are the two methods you’ll virtually alway use when writing code to honor context cancelations.

Subscribe to Boldly Go: Daily

Every day I'll send you advice to improve your understanding of Go. Don't miss out! I will respect your inbox, and honor my privacy policy.

Unsure? Browse the archive.


The Done channel

May I rant for a moment? I don’t like the way GoDoc works for structs and interfaces. And below is a demonstration as to why. It’s not possible to link directly to an interface method or struct field, and the formatting is ugly. sigh type Context type Context interface { … // 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.


Context deadlines

On Friday we looked at the context.Context interface as a whole. Now let’s go back and look at each method in greater detail. type Context type Context interface { // 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. Deadline() (deadline time.Time, ok bool) So there are two general cases here:


The Context API contract

Today we come to the core of the context package: The Context interface itself. type Context type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } A Context carries a deadline, a cancellation signal, and other values across API boundaries. Context’s methods may be called by multiple goroutines simultaneously. For clarity, I’ve removed all of the documentation for each of the interface methods in the above quote—we’ll get to those in following emails, and include those there.


Contexts with timeouts

func WithTimeout func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete: func slowOperationWithTimeout(ctx context.Context) (Result, error) { ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() // releases resources if slowOperation completes before timeout elapses return slowOperation(ctx) } Not only is WithTimeout conceptually similar to WithDeadline, it literally is WithDeadline, as we can see form the source code: