(Go) Contexts

Some good, more bad, mostly ugly

Guy J Grigsby
Star Gazers
4 min readMar 12, 2021

--

Original Go Gopher copyright Renee French

Contexts are a necessary evil they tell us. We need a place to store things like request scoped information. Things like session info need to be propagated. I’ve been mulling over the idea that we should get rid of this type of context all together. It’s just too ripe for abuse. To be clear, I don’t mean that we do away with the Go context.Context. The Go context, when used as designed, is really just a go-routine lifecycle management tool. I am thinking more broadly. I don’t like the idea of an object/struct that holds arbitrary data, most of the time. This post is different than most of mine. I am kind of working through this idea. Let’s explore contexts in the context of Go.

The Good

As mentioned, contexts in Go are built for go-routine lifecycle management. Put simply, they are passed to async functions and go-routines to tell them when they are done. Aside from a few edge cases, that’s the only way I like to use them. If you start to think about them this way, they become very simple to use and it’s clear when (and when not to) use them. For example:

The Bad and Ugly

There are countless examples of bad design in squashed into contexts. It may sounds like a good idea to include an http.Client into a context for micro service frameworks that have to call out to others micro services. (Don’t even get me started on why micro services are bad design to begin with. I feel another post coming on about that later.) It certainly makes it easier for developers to access a preconfigured client, but it pollutes the context. We lose those lovely compile time checks for parameters when a single “kitchen sink” struct is used for passing params.

I worked on a Go project in the past where the context was filled with business logic related fields. At a glance, it was impossible to tell which function was using what attribute. For the benefit of developer convenience you sacrifice:

  • Compile time checks on parameters
  • Function signature readability

In addition, we don’t do a lot of nil checking in Go. Not like Java with null. I think that small structs have a lot to do with that. When you have a huge struct it become unclear what attributes are populated where. A value that you need may or may not be available at a given time. I remember working in Java using Eclipse UI libraries, I can’t remember the name of the UI part, and constantly running into null pointer exceptions because something I wanted to use wasn’t available and it wasn’t clearly documented when or why that value was populated. It was magic. Magic is bad in programming. When it works, it is easy, but when it doesn’t, it takes forever to sort out why. As far as I am concerned, readability and maintainability are the two most important aspects of source code. There are edge cases where scalability and performance outweigh that, but they are few and far between. That’s one of the reasons I love Go. No magic and easy to read.

So if we aren’t going to have contexts with the things we need, how will we do it?

Alternatives

Let’s look back at the example of the need for an http.Client. Maybe we have a giant ecosystem of micro services and need to have a preconfigured client with some custom DNS and login credentials.

  • One great option is to write a library using Go Options. That way we could perform some initialization that is unseen by the user, while at the same time maintaining our compile time checks on structs and API clarity.
  • Another option, one that is no doubt harder, is to review our architecture. If we have created a system of services that need special DNS and a set of login creds, we may want to review why we did that. Could we use a virtual private network combined with bearer tokens instead? Could we bake the DNS settings into our containers by using a base container that everyone else builds on top of?
  • In the case that we have a monolith, we could use a global client. Sort of like a singleton. Singletons get a bad rap because they are hard to mock for testing, but as long as you wrap access to it, you can mock it. I mean look at the Go http.DefaultClient() or http.DefaultServeMux(). They are a lot like singletons.

What are you thoughts? What alternatives have you used to a massive context?

--

--

Guy J Grigsby
Star Gazers

Technologist, taco lover and Gopher. Technologist and software engineer in distributed systems. Neuro-divergent.