Context key/value utilities

July 2, 2025

So we’ve finally settled on a suitable type for our context key types: string, or maybe a non-empty struct.

We also understand that to guard against key collisions, we need our custom type to be un-exported.

type contextKey string

const KeyUserID = contextKey("user_id")

Now code anywhere in your project can use the context key without fear of collision.

Done! Right?

I’m sure you know the fact that I’m asking means there’s more we can do.

To be clear, there are times when it may be appropriate to expose a context key like this. Most often when building a general-purpose library, where you want to give consumers a lot of flexibility.

In most cases, though, we can tighten this up quite a bit, reduce the amount of necessary boiler plate, and make code a bit safer, by adding getter and setter functions in a centralized location:

type contextKey string

const keyUserID = contextKey("user_id") // Note this is now unexported

// WithUserID returns a new context derived from the provided parent context,
// associating it with the specified user ID.
func WithUserID(ctx context.Context, userID int) context.Context {
	return context.WithValue(keyUserID, userID)
}

// UserID returns the user ID associated with this context, or false if none.
func UserID(ctx context.Context) (int, bool) {
	userID, ok := ctx.Value(keyUserID).(int)
	return userID, ok
}

By providing a type-safe getter and setter, we avoid any risk of a consumer of your code accidentally setting a context value to an inappropriate type. e.g.:

var userID int64 = 3
ctx = context.WithValue(ctx, foo.KeyUserID, userID)

/* ... later ... */

userID, ok := ctx.Value(foo.KeyUserID).(int) // ok = false; the type is int64, not int

In some applications, you may want a different getter, or even a set of getters, depending on your needs. Some common patterns:

  • fall back to a default value

    // UserID returns the user ID associated with the context, or -1 if there is
    // none.
    func UserID(ctx context.Context) int {
    	userID, ok := ctx.Value(keyUserID).(int)
    	if !ok {
    		return -1
    	}
    	return userID
    }
    
  • panic if the value isn’t found

    It is convention to prefix such function names with the word Must.

    // MustUserID returns the user ID associated with the context, or panics if none.
    func MustUserID(ctx context.Context) int {
    	userID, ok := ctx.Value(keyUserID).(int)
    	if !ok {
    		panic("no user ID in context")
    	}
    	return userID
    }
    

Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Context key type: Final recommendation

First, a correction! An astute reader pointed out that I made a small mistake in my post on June, 10, with regard to string context keys. My code example showed: type contextKey string const ( KeyUserID = "user_id" KeyTransactionID = "transaction_id" KeyMessageID = "message_id" ) But it should have been: type contextKey string const ( KeyUserID contextKey = "user_id" KeyTransactionID contextKey = "transaction_id" KeyMessageID contextKey = "message_id" ) This is a nasty kind of bug, because the code will continue to work as expected—just without any protection from key collisions!


Struct context keys

First off, an apology for being rather unreliable with my “daily” emails. I’m in the middle of some traveling (was in the UK and Netherlands last week, and will be on a US road trip starting tomorrow), so I’m writing when I have a spare moment, which isn’t that often. I’ve been talking about finding the ideal type for a context key. So far I’ve looked at empty structs (e.g. struct{}), integers, and strings.


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.

Get daily content like this in your inbox!

Subscribe