Abusing type aliases to augment context.Context

tl;dr: I find type aliases a nice add-on to how I (ab)use context.Context. YMMV.

A little while ago I have described my particular way of augmenting context.Context while avoid using context.WithValue .

This time I was writing yet another incarnation of this little trick, and realized that type aliasing would actually allow me to create application specific context hacking more cleanly.

Background

Before writing about how I do this, let me back up and describe my line of thinking.

Application specific code is good

I believe that creating a general-purpose library with a bloated API is a silly idea. I do not wish for something like context.Context to be able to handle all the cases that my particular application needs. This is part of why I don’t like context.WithValue

On the other hand, I think it’s a good idea to create application specific helpers on top of existing simple solutions. They save keystrokes, and if you do it right, you get a cleaner/leaner code base.

In this case, context.Context was going to be available in most of the code path, and most of my code had a user associated with it. Adding request-scoped data like “current user” to context.Context seemed like a good idea.

Opaque values with API to query them

I’m a big fan of this pattern:

That is, when a possible interface X can possibly have many distinct features, I like to define interfaces for each of these, and query the state if the value provides that interface.

When I extend errors, I usually follow this pattern, and create an internal package with those extra functions (see below).

Internal packages

I really like internal packages. When I want to add functionality to existing packages, and when those functionalities are for my application alone, I like to create an internal package that matches the original package name (say, “internal/errors” to match “errors”), and make the internal package look as if it is the original package plus a few more functions or methods.

For example, maybe I want to distinguish fatal and non-fatal errors. I might do something like this:

package errors // internal/errors/errors.goimport "errors"type fatalErr struct {
message string
}
type isFataler struct {
IsFatal() bool
}
func Fatal(s string) error {
return &fatalErr{message: s}
}
func (e *fatalErr) string {
return e.message
}
func (e *fatalErr) IsFatal() bool {
return true
}
func IsFatal(e error) bool {
if fe, ok := e.(isFataler); ok {
return fe.IsFatal()
}
return false
}

This is a very nice pattern that allows you to hide implementation details while keeping a very clean API.

My latest context hack

Back to my context.Context hack. Just like my previous post, I decided to add some data to my context. This includes, but not limited to, stuff like “current user”.

I do NOT want to use context.WithValue. I just don’t. I don’t even want to leave the possibility that my co-workers/co-contributors may access values that I attach to context objects.

So I want to create my own context wrapper. Maybe something like this:

package context // internal package internal/context/context.goimport "context"type withUserCtx struct {
context.Context
user *model.User
}
// WithUser creates a new context with an associated user
func WithUser(ctx context.Context, user *model.User) context.Context {
return &withUserCtx{
Context: ctx,
user: user,
}
}
// User returns the associated user
func (ctx *withUserCtx) User() *model.User {
return ctx.user
}

Notice that because we are embedding context.Context in our struct, we do not need to explicitly implement the methods needed to satisfy the context.Context interface. They will be automatically delegated.

When you use this package, it might look something like this

package mainimport (
"path/to/internal/context"
)
func DoSomethingWithUser(ctx context.Context, userID string) {
user := loadFromDB(userID)
ctx := context.WithUser(ctx, user)
doMoreInterestingStuff(ctx)
}

There are a couple of problems here.

First, our context.WithUser() function returns a context.Context type (instead of its underlying concrete type *withUserCtx because we do not want to make our implementation detail public. Therefore there’s no way to get at the User() method to retrieve the actual user.

The other problem is that context.Context is defined in the stdlib "context" package, and not in our internal package. Therefore if we don’t do something about it, we would need to import two "context" packages, and rename at least one of the packages to something like "mycontext" .

Accessing the associated data

To solve the first problem, we merely need to use the “Opaque value with API to query” pattern. First, we define an interface for things that can return us a *model.User object:

package context // internal/contexttype withUser interface {
User() *model.User
}

Then, we create a public API to query the value if it can give us a user object, and if so, get that value:

func User(ctx context.Context) (*model.User, error) {
if uctx, ok := ctx.(withUser); ok {
if u := uctx.User(); u != nil {
return u, nil
}
}
return nil, errors.New(`no user associated`)
}

With this API, we can query a context for the associated user, regardless of the fact that it is a vanilla context.Context object, or one of our wrapped types.

(Please note that in real usage, we probably need to check for all contexts that we wrapped to find the value that we want. For example, what if we wrapped withUserCtx with withAnotherRandomDataCtx, and queried for the user? withAnotherRandomDataCtx may not implement withUser so we would need to recursively check for a context that implements it. Similarly, we should implment context.WithCancel and the like to respect our augmented features. But I punted that in my code example for now for (1) brevity, and (2) for the fact that this is application-specific, and we do not need a completely generalized code)

Providing a single type definition

To solve the second problem, I previously did something like the following

package context // internal/contextimport "context"type Context interface {
context.Context
}
func WithUser(...) Context { // Context instead of context.Context
...
}

This is how I have been solving the problem before Go 1.9. But after Go 1.9, we could just use type aliases to get a much cleaner definition.

package context // internal/contextimport "context"type Context = context.Context
type CancelFunc = context.CancelFunc

After all, this is precisely what I wanted to describe in my code: “I’m augmenting the stdlib "context" package, but the definitions for context.Context is the same as the one in stdlib”

This still does not solve the problem of having to provide for public API functions such as context.WithCancel, context.WithTimeout, etc, but I think that’s a detail I’m willing to accept.

Conclusions

The final code may look something like this

package context // internal/contextimport (
"context"
"errors"
"path/to/app/model"
)
type Context = context.Context
type CancelFunc = context.CancelFunc
type withUserCtx struct {
context.Context
user *model.User
}
// WithUser creates a new context with an associated user
func WithUser(ctx Context, user *model.User) Context {
return &withUserCtx{
Context: ctx,
user: user,
}
}
// User returns the associated user
func (ctx *withUserCtx) User() *model.User {
return ctx.user
}
// User returns the associated user from ctx. If ctx
// does not implement a User() method, then we assume
// that there was no user associated with the context
// Note: recursive querying omitted for brevity
func User(ctx context.Context) (*model.User, error) {
if uctx, ok := ctx.(withUser); ok {
if u := uctx.User(); u != nil {
return u, nil
}
}
return nil, errors.New(`no user associated`)
}

This approach gives us the following merits:

  1. Using the same "context" package name that we’re accustomed to, we can augment its functionality.
  2. Using the opaque value + query API, we can completely hide the details of our augmented types. We also achieved this with type safety.
  3. Using type aliases allows us to avoid having to declare the same type again (i.e. context.Context).

The downside would be that for novices it would be harder to distinguish what parts are application specific, and what parts are from stdlib.

While I don’t generally recommend using type aliases for anything other than its original purpose (incremental refactoring) for now, I found this particular corner case to be very useful. Please take this post with a grain of salt.

Happy hacking!

Go/perl hacker; author of peco; works @ Mercari; ex-mastermind of builderscon; Proud father of three boys;