Posted on 10 mins read

This is my own personal style guide for Go.

Table of Contents

Reference Materials

The following reference materials are my ‘go to’ whenever I’m unsure of something (they’re mostly official resources).

NOTE: Refer to the specification if ever confused about what the expected behaviour is.

Naming

The following is a summary of how to name things in Go, gleaned from either my own experiences over the years or from some of the above reference materials.

  • Choose package names that lend meaning to the names they export.
  • Where types are descriptive, name should be short (1 or 2 char name).
  • If longer name required, consider refactoring into smaller functions.
  • Commonly used names:
    • Prefer i to index.
    • Prefer r to reader.
    • Prefer buf to buffer.
    • Prefer cfg to config.
    • Prefer dst, src to destination, source.
    • Prefer in, out when referring to stdin/stdout.
    • Prefer rx, tx when dealing with channels.
      • i.e. receiver, transmitter.
    • Prefer data when referring to file content.
      • Regardless of it being a string or []byte.
    • Use ok instead of longer alternatives.
  • Errors:
    • Types: <T>Error (e.g. type ExitError struct {...}).
    • Values: Err<T> (e.g. var ErrFoo = errors.New("bar: baz")).
  • Interfaces:
    • When an interface includes multiple methods, choose a name that accurately describes its purpose.
    • Interfaces that specify just one method are usually just that function name with ’er’ appended to it.
      • Sometimes the result isn’t correct English, that’s OK.
      • Sometimes we use English to make it nicer.
  • Return values on exported functions should only be named for documentation purposes.
    • Side effect is that the variable is initialised at start of function with zero value.
    • This can, in some cases, lead to a nice code design.
  • Set<T> vs Register<T>
    • Set: use when flipping a bit (e.g. setting an int, string etc).
    • Register: use when operation is going into something (e.g. registering a CLI flag inside a command).

NOTE: Refer also to https://github.com/kettanaito/naming-cheatsheet

Whitespace

The go standard library has no strong conventions or idioms for how to handle whitespace. So try and be concise without leaving the user with a wall of text to digest. Additionally, you can use block syntax {...} to help group related logic:

// Simple code is fine to condense the whitespace.
if ... {
  foo
  for x := range y {
    ...
  }
  bar
}

// Complex code could benefit from some whitespace (also separate block syntax for grouping related logic).
if {
  ...

  {
    ...grouping of related logic...
  }

  ...
}

Quick note on Code Design

Not always obvious but be wary of returning concrete types when building a package to be used as a library.

Here is an example of why this might be problematic: we had a library that defined a constructor that returned a struct of type *T. This struct had methods attached and inside of those methods were API calls.

The reason the returning of that struct was a problem was because when we built a separate CLI to consume the package library, we realised our CLI’s test suite wasn’t able to mock the returned type appropriately as some of the fields on the struct were private (these would determine if an attached method would make an API call), and so we were forced to make real API calls!

The solution was for us to return an interface. This made it simple to mock the behaviours we wanted (e.g. we could write our tests to pretend there was an API error, and see how our CLI handled that scenario).

I recommend reading my other post “Thinking about Interfaces in Go”.

Quick guide to Error wrapping

When you wrap errors your message should include:

  • A pointer to where within your method the failure occurred.
  • Values that will be useful during debugging (e.g ids).
  • (sometimes) details about why the error occurred.
  • Other relevant info the caller doesnt know.

And your message should NOT include:

  • The name of your function
  • Any of the arguments to your function
  • Any other information that is already known to the caller

Here is a BAD example where the caller of a function that fails is seeing duplicate information:

// Source
func MightFail(id string) error {
    err := sqlStatement()
    if err != nil {
        return fmt.Errorf("mightFail failed with id %v because of sql: %w", id, err
    }
    ...
    return nil
}

// Caller
func business(ids []string) error {
    for _, id := range ids {
        err := MightFail(id)
        if err != nil {
            return fmt.Errorf("business failed MightFail on id %v: %w", id, err)
        }
    }
}

The resolution to the above bad code is: only include information the caller doesn’t have. The caller is free to annotate your errors with information such as the name of your function, arguments they passed in, etc. There is no need for you to provide that information to them, as its obvious up front. If this same logic is applied consistently you’ll end up with error messages that are high-signal and to-the-point.

Quick guide to panic

  • The use of panic is reserved for when an error is unrecoverable.
  • What constitutes an “unrecoverable” error is contentious. Here are some definitions:
    • To indicate that something impossible has happened, such as exiting an infinite loop.
    • During initialization, if the library truly cannot set itself up, it might be reasonable to panic.
    • When something internally has fundamentally failed.
    • When a programmer gives something to a function which the function explicitly states is invalid.
  • bytes.Truncate is an example of the last sub-point.
    • The above example could be considered aggressive.
    • Instead the standard library could have returned an error so the caller could decide the appropriate action to take.
  • The use (and conditions) of panic should be documented (example: bytes.Truncate)
  • The use of recover is for when you disagree with the library authors.
  • Wherever possible avoid panic and return an error for the caller to handle.

Quick guide to slice ‘gotchas’

When taking a slice of a slice you might stumble into behaviour which appears confusing at first. The cap, len and data fields might change, but the underlying array is not re-allocated, nor copied over and so modifications to the slice will modify the original backing array.

NOTE: There are more examples/explanations in https://blogtitle.github.io/go-slices-gotchas/

Ghost update 1

The underlying array is modified after updating an element on the slice as there is no re-allocation of the underlying array:

a := []int{1, 2}
b := a[:1]     /* [1]     */
b[0] = 42      /* [42]    */
fmt.Println(a) /* [42, 2] */

It’s likely you’ll want to set the capacity when taking a slice of a to assign to b. This will cause a new backing array to be created for the b slice:

a := []int{1, 2}
b := a[:1:2]   // [1]
b[0] = 42
fmt.Println(a) // [42, 2]
fmt.Println(b) // [42]

NOTE: Refer to the golang language specification section on “full slice expressions” syntax ([low : high : max]) for controlling the capacity of a slice.

Ghost update 2

When data gets appended to b (a slice of the a slice), the underlying array has enough capacity to hold two more elements, so append will not re-allocate. This means that appending to b might not only change a but also c (a slice of the a slice).

a := []int{1, 2, 3, 4}
b := a[:2] /* [1, 2] */
c := a[2:] /* [3, 4] */
b = append(b, 5)
fmt.Println(a) /* [1 2 5 4] */
fmt.Println(b) /* [1 2 5]   */
fmt.Println(c) /* [5 4]     */

The ‘fix’, like shown earlier, is b := a[:2:2] which sets the capacity of the b slice such that append will cause a new array to be allocated. This means a will not be modified, nor will the c slice of a.

Quick guide to pass-by-value vs pass-by-pointer

Reference articles: goinbigdata.com and dave.cheney.net.

In essence when people say ‘pass by reference’, the point they’re trying to get across is: “this isn’t a copy of the value being passed”. Where as ‘pass by reference’ is a very specific type of behaviour.

All primitive/basic types (int and its variants, float and its variants, boolean, string, array, and struct) in Go are passed by value.

Maps and slices are passed by pointer (sometimes incorrectly called pass-by-reference). This is where a new copy of the ‘pointer’ to the same memory address is created.

Go does not have pass-by-reference semantics because Go does not have ‘reference variables’ (which is something you’d find in C++).

In C++ you can create a = 10 and then alias b to a (&b = a) such that updating b would affect a. Go doesn’t have this behaviour. Every variable is stored in its own memory space. Meaning if we had b := &a and updated b then we wouldn’t cause any change to a.

When we define a function that accepts a pointer (e.g. changeName(p *Person)) and we pass a pointer to it (e.g. changeName(&person)) the variable person is modified inside the changeName function. This happens because &person and p are two different pointers to the same struct which is stored at the same memory address. This is quite different to C++’s reference variables.

Quick guide to functions with large signature

Your functions should have concise/relevant arguments passed in.

Don’t, for example, pass in an argument whose type is a large and deeply nested object. Firstly, this means the consuming function has to know the structure well enough to dip into it (and arguably it could be argued that this violates the Law of Demeter). Secondly, it makes testing such a function tedious, and thirdly managing such a data structure is equally tedious. Instead choose a field from the object to pass in as it’ll likely have a simpler type (like a string or int).

Three approaches to dealing with functions that potentially could have a large number of arguments…

  1. Make multiple functions to help reduce the number of arguments.
  2. Pass in a <T>Options struct.
  3. Variadic arguments that accept a func type.

I would say go with option 1 whenever possible, and almost never choose option 2 over option 3 as the latter is much more flexible.

The problem with option 2 is that it can become quite cumbersome to construct an object with lots of fields, and more importantly it can be hard to know which fields are required and which are optional. Yes it’s nice that you can easily omit optional fields easily, but then option 3 also provides that benefit while also solving the problem of knowing what arguments are required vs optional.

Using option 3 can be helpful when you want to make the function signature clear, by accepting a couple of concrete arguments that are required for the function to work, while shifting optional arguments into separate functions, as demonstrated below…

type Client struct {
  host, proxy string
  port int
}

type Option func(*Client) // call this function to apply the option

func WithPort(port int) Option {
  return func(c *Client) { c.port = port }
}

func WithProxy(proxy string) Option {
  return func(c *Client) { c.proxy = proxy }
}

func NewClient(host string, options ...Option) *Client {
  c := &Client{host: host, port: 80} // default values
  for _, option := range options {
    option(c) // apply the options by calling each one of them
  }
  return c
}

But before we wrap up... time (once again) for some self-promotion 🙊