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:
-
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 preallocategreeted
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) }
-
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 theslices.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)