Practical Go: Real world advice for writing maintainable Go programs

By Dave Cheney

The first topic we’re going to discuss is identifiers. An identifier is a fancy word for a name; the name of a variable, the name of a function, the name of a method, the name of a type, the name of a package, and so on.

Poor naming is symptomatic of poor design.

— Dave Cheney

Given the limited syntax of Go, the names we choose for things in our programs have an oversized impact on the readability of our programs. Readability is the defining quality of good code thus choosing good names is crucial to the readability of Go code.

Obvious code is important. What you can do in one line you should do in three.

— Ukiah Smith

Go is not a language that optimises for clever one liners. Go is not a language which optimises for the least number of lines in a program. We’re not optimising for the size of the source code on disk, nor how long it takes to type.

Good naming is like a good joke. If you have to explain it, it’s not funny.

— Dave Cheney

Key to this clarity is the names we choose for identifies in Go programs. Let’s talk about the qualities of a good name:

  • A good name is concise. A good name need not be the shortest it can possibly be, but a good name should waste no space on things which are extraneous. Good names have a high signal to noise ratio.

  • A good name is descriptive. A good name should describe the application of a variable or constant, not their contents. A good name should describe the result of a function, or behaviour of a method, not their operation. A good name should describe the purpose of a package, not its contents. The more accurately a name describes the thing it identifies, the better the name.

  • A good name is should be predictable. You should be able to infer the way a name will be used from its name alone. This is a function of choosing descriptive names, but it also about following tradition. This is what Go programmers talk about when they say idiomatic.

Let’s talk about each of these properties in depth.

Sometimes people criticise the Go style for recommending short variable names. As Rob Pike said, "Go programmers want the right length identifiers".

Andrew Gerrand suggests that by using longer identifies for some things we indicate to the reader that they are of higher importance.

The greater the distance between a name’s declaration and its uses, the longer the name should be.

— Andrew Gerrand

From this we can draw some guidelines:

  • Short variable names work well when the distance between their declaration and last use is short.

  • Long variable names need to justify themselves; the longer they are the more value they need to provide. Lengthy bureaucratic names carry a low amount of signal compared to their weight on the page.

  • Don’t include the name of your type in the name of your variable.

  • Constants should describe the value they hold, not how that value is used.

  • Single letter variables for loops and branches, single words for parameters and return values, multiple words for functions and package level declarations

  • Single words for methods, interfaces, and packages.

  • Remember that the name of a package is part of the name the caller uses to to refer to it, so make use of that.

Let’s look at an example to

type Person struct { Name string Age int
} // AverageAge returns the average age of people.
func AverageAge(people []Person) int { if len(people) == 0 { return 0 } var count, sum int for _, p := range people { sum += p.Age count += 1 } return sum / count
}

In this example, the range variable p is declared on line 10 and only referenced on the following line. p lives for a very short time both on the page, and during the execution of the function. A reader who is interested in the effect values of p have on the program need only read two lines.

By comparison people is declared in the function parameters and lives for seven lines. The same is true for sum, and count, thus they justify their longer names. The reader has to scan a wider number of lines to locate them so they are given more distinctive names.

I could have chosen s for sum and c (or possibly n) for count but this would have reduced all the variables in the program to the same level of importance. I could have chosen p instead of people but that would have left the problem of what to call the for …​ range iteration variable. The singular person would look odd as the loop iteration variable which lives for little time has a longer name than the slice of values it was derived from.

Tip

Use blank lines to break up the flow of a function in the same way you use paragraphs to break up the flow of a document. In AverageAge we have three operations occurring in sequence. The first is the precondition, checking that we don’t divide by zero if people is empty, the second is the accumulation of the sum and count, and the final is the computation of the average.

It’s important to recognise that most advice on naming is contextual. I like to say it is a principle, not a rule.

What is the difference between two identifiers, i, and index. We cannot say conclusively that one is better than another, for example is

for index := 0; index < len(s); index++ { //
}

fundamentally more readable than

for i := 0; i < len(s); i++ { //
}

I argue it is not, because it is likely the scope of i, and index for that matter, is limited to the body of the for loop and the extra verbosity of the latter adds little to comprehension of the program.

However, which of these functions is more readable?

func (s *SNMP) Fetch(oid []int, index int) (int, error)
func (s *SNMP) Fetch(o []int, i int) (int, error)

In this example, oid is an abbreviation for SNMP Object ID, so shortening it to o would mean programmers have to translate from the common notation that they read in documentation to the shorter notation in your code. Similarly, reducing index to i obscures what i stands for as in SNMP messages a sub value of each OID is called an Index.

Tip

Don’t mix and match long and short formal parameters in the same declaration.

You shouldn’t name your variables after their types for the same reason you don’t name your pets "dog" and "cat". You also probably shouldn’t include the name of your type in the name of your variable’s name for the same reason.

The name of the variable should describe its contents, not the type of the contents. Consider this example:

var usersMap map[string]*User

What’s good about this declaration? We can see that its a map, and it has something to do with the *User type, that’s probably good. But usersMap is a map, and Go being a statically typed language won’t let us accidentally use it where a scalar variable is required, so the Map suffix is redundant.

Now, consider what happens if we were to declare other variables like:

var ( companiesMap map[string]*Company productsMap map[string]*Products
)

Now we have three map type variables in scope, usersMap, companiesMap, and productsMap, all mapping strings to different types. We know they are maps, and we also know that their map declarations prevent us from using one in place of another—​the compiler will throw an error if we try to use companiesMap where the code is expecting a map[string]*User. In this situation it’s clear that the Map suffix does not improve the clarity of the code, its just extra boilerplate to type.

My suggestion is to avoid any suffix that resembles the type of the variable.

Tip

If users isn’t descriptive enough, then usersMap won’t be either.

This advice also applies to function parameters. For example:

type Config struct { //
} func WriteConfig(w io.Writer, config *Config)

Naming the *Config parameter config is redundant. We know its a *Config, it says so right there.

In this case consider conf or maybe c will do if the lifetime of the variable is short enough.

If there is more that one *Config in scope at any one time then calling them conf1 and conf2 is less descriptive than calling them original and updated as the latter are less likely to be mistaken for one another.

Note

Don’t let package names steal good variable names.

The name of an imported identifier includes its package name. For example the Context type in the context package will be known as context.Context. This makes it impossible to use context as a variable or type in your package.

func WriteLog(context context.Context, message string)

Will not compile. This is why the local declaration for context.Context types is traditionally ctx. eg.

func WriteLog(ctx context.Context, message string)

Another property of a good name is it should be predictable. The reader should be able to understand the use of a name when they encounter it for the first time. When they encounter a common name, they should be able to assume it has not changed meanings since the last time they saw it.

For example, if your code passes around a database handle, make sure each time the parameter appears, it has the same name. Rather than a combination of d *sql.DB, dbase *sql.DB, DB *sql.DB, and database *sql.DB, instead consolidate on something like;

Doing so promotes familiarity; if you see a db, you know it’s a *sql.DB and that it has either been declared locally or provided for you by the caller.

Similarly for method receivers; use the same receiver name every method on that type. This makes it easier for the reader to internalise the use of the receiver across the methods in this type.

Note

The convention for short receiver names in Go is at odds with the advice provided so far. This is just one of the choices made early on that has become the preferred style, just like the use of CamelCase rather than snake_case.

Tip

Go style dictates that receivers have a single letter name, or acronyms derived from their type. You may find that the name of your receiver sometimes conflicts with name of a parameter in a method. In this case, consider making the parameter name slightly longer, and don’t forget to use this new parameter name consistently.

Finally, certain single letter variables have traditionally been associated with loops and counting. For example, i, j, and k are commonly the loop induction variable for simple for loops. n is commonly associated with a counter or accumulator. v is a common shorthand for a value in a generic encoding function, k is commonly used for the key of a map, and s is often used as shorthand for parameters of type string.

As with the db example above programmers expect i to be a loop induction variable. If you ensure that i is always a loop variable, not used in other contexts outside a for loop. When readers encounter a variable called i, or j, they know that a loop is close by.

Tip

If you found yourself with so many nested loops that you exhaust your supply of i, j, and k variables, its probably time to break your function into smaller units.

Go has at least six different ways to declare a variable

  • var x int = 1

  • var x = 1

  • var x int; x = 1

  • var x = int(1)

  • x := 1

I’m sure there are more that I haven’t thought of. This is something that Go’s designers recognise was probably a mistake, but its too late to change it now. With all these different ways of declaring a variable, how do we avoid each Go programmer choosing their own style?

I want to present a suggestions for how I declare variables in my programs. This is the style I try to use where possible.

  • When declaring, but not initialising, a variable, use var. When declaring a variable that will be explicitly initialised later in the function, use the var keyword.

    var players int // 0 var things []Thing // an empty slice of Things var thing Thing // empty Thing struct
    json.Unmarshall(reader, &thing)

    The var acts as a clue to say that this variable has been deliberately declared as the zero value of the indicated type. This is also consistent with the requirement to declare variables at the package level using var as opposed to the short declaration syntax—​although I’ll argue later that you shouldn’t be using package level variables at all.

  • When declaring and initialising, use :=. When declaring and initialising the variable at the same time, that is to say we’re not letting the variable be implicitly initialised to its zero value, I recommend using the short variable declaration form. This makes it clear to the reader that the variable on the left hand side of the := is being deliberately initialised.

To explain why, Let’s look at the previous example, but this time deliberately initialising each variable:

var players int = 0 var things []Thing = nil var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)

In the first and third examples, because in Go there are no automatic conversions from one type to another; the type on the left hand side of the assignment operator must be identical to the type on the right hand side. The compiler can infer the type of the variable being declared from the type on the right hand side, to the example can be written more concisely like this:

var players = 0 var things []Thing = nil var thing = new(Thing)
json.Unmarshall(reader, thing)

This leaves us with explicitly initialising players to 0 which is redundant because 0 is `players’ zero value. So its better to make it clear that we’re going to use the zero value by instead writing

What about the second statement? We cannot elide the type and write

Because nil does not have a type. Instead we have a choice, do we want the zero value for a slice?

or do we want to create a slice with zero elements?

var things = make([]Thing, 0)

If we wanted the latter then this is not the zero value for a slice so we should make it clear to the reader that we’re making this choice by using the short declaration form:

things := make([]Thing, 0)

Which tells the reader that we have chosen to initialise things explicitly.

This brings us to the third declaration,

Which is both explicitly initialising a variable and introduces the uncommon use of the new keyword which some Go programmer dislike. If we apply our short declaration syntax recommendation then the statement becomes

Which makes it clear that thing is explicitly initialised to the result of new(Thing)--a pointer to a Thing--but still leaves us with the unusual use of new. We could address this by using the compact literal struct initialiser form,

Which does the same as new(Thing), hence why some Go programmers are upset by the duplication. However this means we’re explicitly initialising thing with a pointer to a Thing{}, which is the zero value for a Thing.

Instead we should recognise that thing is being declared as its zero value and use the address of operator to pass the address of thing to json.Unmarshall

var thing Thing
json.Unmarshall(reader, &thing)

Note

Of course, with any rule of thumb, there are exceptions. For example, sometimes two variables are closely related so writing

Would be odd. The declaration may be more readable like this

  • When declaring a variable without initialisation, use the var syntax.

  • When declaring and explicitly initialising a variable, use :=.

Tip

Make tricky declarations obvious.

When something is complicated, it should look complicated.

Here length may be being used with a library which requires a specific numeric type and is more explicit that length is being explicitly chosen to be uint32 than the short declaration form:

In the first example I’m deliberately breaking my rule of using the var declaration form with an explicit initialiser. This decision to vary from my usual form is a clue to the reader that something unusual is happening.

I talked about a goal of software engineering to produce readable, maintainable, code. Therefore you will likely spend most of your career working on projects of which you are not the sole author. My advice in this situation is to follow the local style.

Changing styles in the middle of a file is jarring. Uniformity, even if its not your preferred approach, is more valuable for maintenance than your personal preference. My rule of thumb is; if it fits through gofmt then its usually not worth holding up a code review for.

Tip

If you want to do a renaming across a code-base, do not mix this into another change. If someone is using git bisect they don’t want to wade through thousands of lines of renaming to find the code you changed as well.