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 }