Go common pitfall #1: slice appending

Go's slice is simple to use, but there are some easy to make mistakes if you are not aware of its internals.

For example, with following program:

package main

import "fmt"

func main() {
    healthyFoods := []string{"orange", "apple", "lettuce"}
    myFoods := healthyFoods[0:2]
    myFoods = append(myFoods, "bacon")
    fmt.Println(healthyFoods)
}

You might think that it would print [orange apple lettuce], but the actual output is: [orange apple bacon]. It is as dangerous as you can see, bacon is definitely not healthy!

Understand the problem

To understand why it happens, it is recommended to read this excellent blog post: Go Slices: usage and internals.

The healthyFoods slice is created with:

  • length: 3
  • capacity: 3

It is backed by an array with length equals to 3.

The myFoods slice is created with:

  • length: 2
  • capacity: 3 (the capacity of the backing array)

An important thing to keep it mind is, healthyFoods and myFoods backed by the same array.

This is what happened when appending "bacon" to myFoods slice:

  • Go sees that myFoods hasn't used all of its capacity (2 < 3)
  • "bacon" is written to the slot third slot in the backing array, which  "lettuce" is currently located

That is the reason healthyFoods changed, even though we never directly did anything with it.

How to fix it?

Understanding the problem, the easiest way to fix it is explicitly set the capacity when slicing a slice.

myFoods := healthyFoods[0:2:2]

There we go, our program is now printing the expected [orange apple lettuce] output. This form of slicing is called full slice expressions. IMO, this feature should be used more often that it should.

What happened when appending "bacon" to myFoods now:

  • Go sees that myFoods has used up all of its capacity (2 = 2)
  • Go creates another backing array and copy myFoods's elements over

Therefore, the original healthyFoods is left alone untouched.