Anton Lindstrom (about, @twitter, @github)

Go func: Reducing cognitive load


I have been maintaining several fairly large Go projects for a while. Some of these projects have been for web services (APIs as well as backend workers) and some have been command line tools. A few of these projects have been new projects (green field) where I led the design and implementation, some have been taking over maintenance and some have been projects where I contributed.

One major thing that I have learned across these projects is that even though the code format (go fmt) is set, the layout and design have been quite different. This is perfectly fine where the requirements differ. However I want to cover some things that I have noticed that makes it easier for developers to pick up and read code.

This post will cover a part that makes it hard for me to understand where configuration comes from. Reading the code path is essential for me to understand how a program functions and how to debug it.

Good code for me is code that is easy to read, that is the number one property. Writing abstractions to make it easier to write code fast and efficiently becomes unimportant when comparing to readability.

To reduce cognitive load, I want to be able to read a function/method without having to think about where some variable comes from. The input and output should be well defined. Let me give an example:

package example

import "config"

func Circumference(radius float64) float64 {
    return 2*config.Pi*radius

The most common pattern I have seen is that a config package is defined and is imported in many different packages, changed by either flags or somewhere else (or in the worst of cases, both).

When I read the code above, my first question is: How is config.Pi defined?

From this example, I can guess that config.Pi is set to something like 3.14... but is it set to a more exact value? Does it use math.Pi? Is it set manually from a flag or file?

One example of getting rid of these questions is to define the signature to get rid of the config package and call the function with pi set. This way we know that we have to provide the value ourselves:

package example

func Circumference(pi float64, radius float64) float64 {
    return 2*pi*radius

Another way is to define a default value, this way I can determine what the value is but still be allowed to overwrite it. I would still recommend have the call signature define the pi to be able to keep readability.

package example

var DefaultPi float64 = 3.141592

func Circumference(radius float64) float64 {
    return 2*DefaultPi*radius

If you know that you will not change pi to something of your liking, either change DefaultPi to pi (but most likely you want to use math.Pi directly).


In order to reduce cognitive load and make it easier for new developers to understand and read your code, be explicit of what you use in your signatures. Code readability should be your top priority.

See also