Length and capacity in Go slices

Length and capacity in Go slices confused me for a while. They're related but different, and important. This will give you more clarity on what they are.

Photo of a clean warehouse painted white with open shelving storing open boxes of probably food.
Photo by Petr Magera on Unsplash

You may have noticed that when you create a slice in Go, you can use two different ways of doing that. You may optionally pass in an argument for capacity. Let’s talk about why that’s optional, and what does that even mean.

The two (four) methods

Both of these will use the make function. Here’s the first one:

func main() {
    my_slice := make([]string, 0)
}

And here’s the other one:

func main() {
    my_slice := make([]string, 0, 16)
}

Oh, and what even happens if you pass non-zero as the second argument, like this:

func main() {
    my_slice := make([]string, 6)
}

And will Go just break if you pass the third argument as a lower number as the second argument, like this:

func main() {
    my_slice := make([]string, 6, 4)
}

What even are length and capacity?

To answer that we need to first know what slices are. Slices are pointers to a part of an array of things (string, integer, your custom struct type, whatever) in memory. The Go documentation has a deep dive on what they are which you can read here: https://go.dev/blog/slices-intro.

That means that slices are never stored in memory. Arrays are. Slices are just a window to what’s in memory. The consequence of that is that when you create a slice, a backing array also needs to be created, otherwise your computer won’t have a space to store your elements in memory.

How big that backing array is going to be depends on how you create your slice.

Creating a zero length slice, not specifying capacity

Using my_s := make([]string, 0) will create a relatively small backing array, and the slice will have zero elements in it. That also means the backing array has zero elements in it. Think of it like going to a self storage place, renting the smallest unit, and staring at the empty locker.

You start adding elements to it with append:

func main() {
    my_s := make([]string, 0);

    my_s = append(my_s, "something")
    ...
}

That’s roughly the same as starting to put stuff into your tiny locker. At some point you’ll run out of space. The Go runtime will recognise that, create a new, larger backing array, create a new slice that points to that, and copy over your existing elements.

The analogy is you went to the office of the self storage, rented a unit that’s the next size up, and moved all your things into the new unit. It’s now half full, but you have space to pack things into it.

As you add more elements, you’ll run out of space here too, so you’ll need to upgrade to a bigger unit / backing array, and so on.

How big of a buffer to give you on sizing up is a balance the runtime needs to keep between not allocating too much memory at once, and giving you enough to work with so it doesn’t need to do this often.

Creating a zero length slice, and specifying capacity

This would be the following case: my_s := make([]string, 0, 16). The difference here is you passed in 16 as your third argument. You told Go that you want your backing array to be 16 elements long.

func main() {
    my_s := make([]string, 0, 16)

    for i := 0; i < 16; i++ {
        my_s = append(my_s, "item")
    }

    ...
}

In this case the backing array is already 16 elements long, so every individual append will go into the same backing array without Go needing to reallocate a new, larger one.

In the self storage analogy, you show up to the office and immediately take out a medium sized room. You start putting your things in it and they are tiny when compared to the size of the room, but as you keep adding more and more stuff, you won’t need to upgrade to a larger room for a while.

Of course once you reach capacity, the same process happens as earlier: you do have to find a new, larger one, move all your existing stuff to it, and then keep adding to that.

Creating a slice with length, not specifying capacity

That would be this case: my_s := make([]string, 6). In that case Go creates a backing array that’s exactly 6 elements long, and the slice is also 6 elements long with each element being the zero value of the type. In this case the type of the elements is string, the zero value of a string is emptystring, so you’ll have this as a slice: {"", "", "", "", "", ""}.

You show up to the self storage, tell them you are going to store 6 boxes worth of stuff. They give you a room that can fit exactly those 6 boxes, give you the boxes, and you put all 6 empty boxes in there. You can put stuff in the boxes, ie change their content, but the room is technically full, so if you want to add a new box, you need to move to a bigger room / backing array.

The main difference between this case, and my_s := make([]string, 0, 6) is that the slice itself is going to have a length as well, not just the backing array. In both cases the backing array is going to be 6 long.

Creating a slice with length and capacity

For example my_s := make([]string, 6, 32). This would create a slice with 6 elements of emptystring, with a backing array of 32 spaces, 6 of which are taken up.

Warehouse analogy: you got the medium room, you have 6 empty boxes inside of it already. You can put stuff into those boxes, and you can keep adding more boxes until you fill up the room before needing to move to a bigger one.

Note: if you try to create a slice where the length is more than the capacity, like my_s := make([]string, 6, 4), Go will panic and tell you that you probably swapped your arguments. You can’t fit 6 boxes into a room that only has space for 4 of them.

All of this is very much a generalisation over what’s actually happening. The previously linked intro to slices is an excellent resource if you want to know more, and should you want to dive a lot deeper, you can check out the following: