<jack>

If it’s too hard you’re doing it wrong.

The Context Overridable Global Pattern in Go

Posted at — Apr 11, 2020

Global variables are almost universally discouraged in software development — and for good reason. Storing data in a context is often discouraged as well — and again for good reason. However, global variables and context values can be combined to produce something that preserves the strengths and minimizes the weaknesses of each.

The Problem With Global Variables

One of the earliest rules a software developer learns is to avoid global variables. But global values do exist. Most applications have at least several variables that could be considered globals such as a database connection pool, a logger, or an application configuration object. Technically, they may or may not actually be global variables. The developer may scrupulously follow “best practices” and instead pass the value as an argument or capture it in a closure. But practically speaking, they are global values. There is only one application configuration object.

The big problem with a global variable is the “variable” not the “global”. The downside of a global immutable value is small. But usually values like an application configuration object are initialized on program startup and do not change afterwards.

But the “global” part can cause problems as well. For example, when the actual application is running it makes sense for there to be one global configuration object. But global variables make it much more difficult to write tests. Another example would be a logger that is used almost everywhere, but on occasion should be overridden.

The Problem With Context Values

Context values have multiple issues as well. Type safety is lost because they are stored and retrieved as interface{}. In addition, functions that rely on context values being present essentially have hidden required arguments. Lastly, there is a performance penalty for a context with many values.

The Context Overridable Global Pattern

The context overridable global pattern restricts access to globals to getter functions. These functions accept a context as an argument. If the context has a value for that global then the context value is returned. If not the global is returned. For example, the sample below has an context overridable global of AppName.

package main

import (
	"context"
	"ctxglobexample/app"
	"fmt"
)

func main() {
	app.SetAppName("Global Value")

	// The background context will use the global value.
	fmt.Println("Background context:", app.AppName(context.Background()))

	// A context can override the global value.
	ctx := app.WithAppName(context.Background(), "Context Value")
	fmt.Println("WithAppName context:", app.AppName(ctx))

	// The global value is unchanged.
	fmt.Println("Background context:", app.AppName(context.Background()))
}

Output:

Background context: Global Value
WithAppName context: Context Value
Background context: Global Value

And here is the implementation.

package app

import (
	"context"
)

// Use internal types and values as context keys to enforce access only through exposed functions.
type ctxKey int

const (
	_ ctxKey = iota
	appNameCtxKey
)

var appName string

func SetAppName(s string) {
	if appName != "" {
		panic("cannot call appName twice")
	}
	if s == "" {
		panic("AppName must not be empty")
	}
	appName = s
}

func AppName(ctx context.Context) string {
	v := ctx.Value(appNameCtxKey)
	if v != nil {
		return v.(string)
	}

	if appName == "" {
		panic("missing AppName in ctx and global")
	}

	return appName
}

func WithAppName(ctx context.Context, s string) context.Context {
	return context.WithValue(ctx, appNameCtxKey, s)
}

This pattern solves the problems identified above with globals and contexts.

  1. Global variables are mutable — global values can only be set once. If mutability was really desired, then the getter and setter functions can be safely altered to use a mutex.
  2. Global variables are global — the context can be used to override the global whenever needed.
  3. Context values lose type safety — access is restricted to getter and setter functions that are type safe.
  4. Functions relying on context values have hidden required arguments — the global value is used if the context value is missing. The required argument is now an optional argument.
  5. Performance penalty of many values on a context — the almost global values are rarely set on the context. They fall through to the global without ever using context.WithValue().