Efficiently appending to slices

August 30, 2024

Yesterday we were introduced to the append function, which on the surface seems quite straight forward.

But there are nuänces and gotchas!

Appending to and copying slices

If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values.

You may recall from our (much) earlier discussion about slices and backing arrays, that a slice has not only a length (the number of elements it contains), but also a capacity—essentially the number of available elements in the backing array. The capacity may be larger than the length:

a := [10]string{}
s := a[0:3]
fmt.Println(len(s), cap(s)) // Prints 3 10

When appending to a slice, if the capacity would be exceeded, then a new backing array is created, which is large enough to old slice, and all the appended elements, and that is returned:

s := []int{1, 2, 3}
fmt.Println(len(s), cap(s)) // Prints 3 3
s = append(s, 4, 5, 6)
fmt.Println(len(s), cap(s)) // Prints 6 6

This can have a (in some cases, severe) performance impact, so it’s good to be mindful of this.

Every time a new slice is allocated, and the old one discarded, the garbage collector has to clean up the old one. If you do this in a tight loop, often enough, it could have a potential impact on your program’s performance.

So what can you do about it?

Two things I’ll suggest:

  1. Preallocate slices, when it’s reasonable.

    A common requirement is to do some operation on every item in a slice, and store the result in a new slice.

    names := []string{"Bob", "Alice", "Chuck", "Dave"}
    greeted := []string{}
    for _, name := range names {
    	greeted = append(greeted, "Hello, "+ name)
    }
    

    This loop has the potential to create, and discard, four slices, one for each append to greeted. But we can preallocate greeted with the size we know we’ll need, and then only the single, final slice is created:

    names := []string{"Bob", "Alice", "Chuck", "Dave"}
    greeted := make([]string, 0, len(names))
    for _, name := range names {
    	greeted = append(greeted, "Hello, "+ name)
    }
    
  2. Use slices.Concat

    Another trick you can do, which comes in handy when you need to do multiple append calls at once, is to instead use the slices.Concat function, which was added in Go 1.22. Instead of this:

    names := []string{"Bob", "Alice", "Chuck", "Dave"}
    names = append(names, namesReadFromTheDatabase)
    names = append(names, otherNamesReadFromAnAPI)
    

    Do this:

    names := slices.Concat([]string{"Bob", "Alice", "Chuck", "Dave"},
    	namesReadFromTheDatabase,
    	otherNamesReadFromAnAPI,
    )
    

    This will do the append/concat operation quite efficiently.

Quotes from The Go Programming Language Specification Language version go1.23 (June 13, 2024)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


Making slices, maps and channels

Making slices, maps and channels The built-in function make takes a type T, optionally followed by a type-specific list of expressions. The core type of T must be a slice, map or channel. It returns a value of type T (not *T). The memory is initialized as described in the section on initial values. Call Core type Result make(T, n) slice slice of type T with length n and capacity n make(T, n, m) slice slice of type T with length n and capacity m make(T) map map of type T make(T, n) map map of type T with initial space for approximately n elements make(T) channel unbuffered channel of type T make(T, n) channel buffered channel of type T, buffer size n The only time you absolutely need to use make is when creating channels.


Beware of array reuse with append

On Friday we looked at what happens when appending to a slice exceeds the capacity of the backing array. Today we’ll consider the alternative: What if the backing array can hold the new values? Appending to and copying slices … Otherwise, append re-uses the underlying array. Okay. That sounds pretty simple, doesn’t it? What’s to talk about? Well, this single sentence packs a lot of surprises. Consider this code: a := [.


Appending to slices

Appending to and copying slices The built-in functions append and copy assist in common slice operations. For both functions, the result is independent of whether the memory referenced by the arguments overlaps. Today and tomorrow we’ll look at the first of those two, append: The variadic function append appends zero or more values x to a slice s and returns the resulting slice of the same type as s. The core type of s must be a slice of type []E.

Get daily content like this in your inbox!

Subscribe