Golang Project Structure

Tips and tricks for writing and structuring Go code

Adding Elements to a Slice

Language

  • unknown

by

This post will discuss the various ways to add elements to a slice. Appending an element adds it to the end of the slice, whereas prepending it adds it to the beginning of a slice. It is also, of course, possible to add an element at any position in between. We will finish by looking at how to clone a slice, so that all its elements are copied into a new slice.

Previous Posts About Slice Manipulation

If you haven’t already read them, you may want to have a look at the two posts I’ve already written about how to manipulate slices.

The first post discusses how to reverse the order of all the elements in a slice. The second one talks about the various ways to remove elements from a slice, so it serves as a good companion piece to the current post about adding elements.

However, it really doesn’t matter which order you read any of these three posts in, so you can carry on reading this page and go back to look at them later, if you like.

You can, of course, also skim through the various code examples and just look for a specific solution to your problem, if you came to this page via a search engine after struggling with your own code. All of the posts I’ve written about slice manipulation are intended to be useful both for reference purposes and also for reading in full.

Appending an Element to a Slice

The idiomatic way to add an element to the end of a slice in Go involves using the append built-in function.

The append function takes at least two arguments: the first argument is the slice that you want to append to and the second argument is the element that you want to append. It is a variadic function, so you can pass as many elements to the function as you like, after the second argument, if you want to append more than one element.

Needless to say, the elements that you wish to append to the slice must be of the same type that the slice was initialized to hold.

The append function returns the updated slice, which will have a larger length may occupy a completely different position in memory, if the original slice didn’t have sufficient capacity to hold the new elements. So it is important always to overwrite the original slice with the return value of the append function, if you want to modify it.

In fact, the Go compiler will fail to build your code if you do not assign the return value to a variable, spitting out an error message that looks something like this (when trying to append the element 123 to a slice of integers):

append(slice, 123) (value of type []int) is not used

Look at the following example:

package main

import (
	"fmt"
	"strings"
)

func main() {
	friends := []string{"John", "Paul", "George"}

	fmt.Println("BEFORE APPENDING")
	fmt.Printf("length: %d\n", len(friends))
	fmt.Println(friends)

	friends = append(friends, "Ringo")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println("AFTER APPENDING")
	fmt.Printf("length: %d\n", len(friends))
	fmt.Println(friends)
}

We start out with a slice containing the names of three friends. We print out the length of the slice and its contents, so we can see how they change after the append function is called.

We then add the name of a fourth friend to the slice, overwriting the original variable. This will increase the capacity of the slice, if it isn’t already large enough, in which case the slice will simply be resliced to increase its length.

Finally, we print out the length and the contents of the slice once more, so we can see that the length has increased by one and "Ringo" has been added at the end.

(The strings.Repeat function is simply used to create a long line of twenty-five asterisk characters, in order to separate the two parts of our output.)

Appending Elements to a Slice With Preallocated Memory

As we mentioned in the previous section, the capacity of the slice will increase after a call to append, if there isn’t enough room to add the extra element in memory. In the worst-case scenario, this could involve all of the existing elements in the slice having to be moved to a new area of memory before the append can be carried out.

For example, try running the following code, which prints out the capacity of the friends slice before and after another element is appended to it:

package main

import (
	"fmt"
	"strings"
)

func main() {
	friends := []string{"Ringo", "George", "Paul"}

	fmt.Println(cap(friends))

	friends = append(friends, "John")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(cap(friends))
}

The output may vary depending on your machine and version of Go, but when I run it, I get the following result:

3
*************************
6

The size of the underlying memory used by the slice doubles when a single element is appended. The fact that our existing elements may need to be moved around in memory before the new one is added does not pose a huge problem in this case, since our slice is so small, but if it had contained many more elements, then it could have really caused the performance to take a noticeable hit.

So if you know how many elements your slice is going to contain, it’s better to preallocate memory using the make builtin function when you first initialize the slice.

This is what we do in the example below:

package main

import (
	"fmt"
	"strings"
)

func main() {
	friends := make([]string, 0, 4)

	fmt.Println(len(friends), cap(friends))

	friends = append(friends, "John", "Paul", "George")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))

	friends = append(friends, "Ringo")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))
}

When we run this, we should now see that whenever the length increases, the capacity stays the same, because it is always big enough to hold all four elements. This allows us to be confident that the memory won’t need to be reorganized, unless we choose to add more than four elements (or set a greater capacity to begin with). The output that I got is shown below:

0 4
*************************
3 4
*************************
4 4

Our use of the make function isn’t too hard to understand. The first argument contains the type of slice that we want to initialize. The second argument is the length of the slice, which we set to zero, since it has no elements. The third argument is the capacity, which is the total amount of items that the slice could potentially hold without needing to be reallocated.

Adding Elements to a Slice With a Predetermined Length

We could also make things even easier by simply setting a length for our slice and letting the compiler infer the capacity that we need, as shown below:

package main

import (
	"fmt"
	"strings"
)

func main() {
	friends := make([]string, 4)

	fmt.Println(len(friends), cap(friends))

	friends[0] = "John"
	friends[1] = "Paul"
	friends[2] = "George"

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))

	friends[3] = "Ringo"

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))
}

The length and capacity should now remain stable throughout the program’s life, as seen in the output below:

4 4
*************************
4 4
*************************
4 4

Note that we had to set the elements by slice, because the memory had already been allocated. If we had used the append function, it would have added the elements beyond the four slots already allocated, leaving them empty, which would entirely defeat the purpose of preallocating memory.

Look at the following bad code, for example:

package main

import (
	"fmt"
	"strings"
)

func main() {
	friends := make([]string, 4)

	fmt.Println(len(friends), cap(friends))

	friends = append(friends, "John", "Paul", "George")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))

	friends = append(friends, "Ringo")

	fmt.Println(strings.Repeat("*", 25))
	fmt.Println(len(friends), cap(friends))
}

This increases the capacity, because when we append elements to a slice, we are always adding them beyond the current length, even if there are free slots we could have used. You can see in the output below how the capacity has now needlessly doubled:

4 4
*************************
7 8
*************************
8 8

So always use index-based assignment when you want have a slice that already has a large length you want to make use of. Otherwise, use the append function to increase the length of the slice.

Prepending Elements to a Slice

You don’t always want to add an element to the back of a slice, however. Sometimes you want to add it to the front. The problem is that there is no builtin function to prepend an element to a slice, as there is to append one.

So we have to find a way to achieve it ourselves. The simplest way is to append all of the existing elements of your slice to a new slice containing only the element (or elements) that you want to prepend and then assign that new slice to the original variable. This is what we do in the code below:

package main

import "fmt"

func main() {
	fibonacciNumbers := []int{1, 2, 3, 5, 8, 13, 21}

	fibonacciNumbers = append([]int{1}, fibonacciNumbers...)

	fmt.Println(fibonacciNumbers)
}

If you run this code, you will see that the fibonnaciNumbers slice will now contain an extra one at the beginning.

All of the original elements had to be copied to a new slice, which could have been a pretty expensive operation, if the original slice had been very large.

There is, however, little way to avoid this, since there is no way to prepend an element to a slice in Go without moving the other elements. This is why you should always append elements to the end of a slice, unless you have a very good reason for inserting them into the slice elsewhere.

(By the way, since I used them in this example, if you want to learn more about what the Fibonacci numbers are and how they can be generated programmatically, please read my post on the topic.)

A Slightly More Efficient Way to Prepend Elements to a Slice

Even though prepending elements to a slice will always be a relatively expensive operation in Go, there is a slight improvement we can make to our code. Look at the example below:

package main

import "fmt"

func main() {
	fibonacciNumbers := []int{1, 2, 3, 5, 8, 13, 21}

	fibonacciNumbers = append(fibonacciNumbers, 0)

	copy(fibonacciNumbers[1:], fibonacciNumbers)

	fibonacciNumbers[0] = 1

	fmt.Println(fibonacciNumbers)
}

Instead of creating a new slice to append the original elements after the new element, as we did in the previous example, we now simply add an extra element to the original slice. In this case, we add a zero, but it doesn’t really matter what it is, because we’re going to overwrite it: we’re only using it to make sure the capacity of our slice will grow, if necessary, to be large enough to hold another element.

Then we shift all of the existing elements up a place in the original slice, using the copy function. We reslice fibonacciNumbers in order to do this. The copy function stops when it reaches the end of either the destination or the source slice: the resliced destination slice is one element shorter than the source slice, so it will not copy over the zero that we appended just for memory-management reasons.

Note that the example above will never need to allocate any new memory so long as the original slice has enough capacity to hold another element, whereas the example above will need to allocate memory, since we were creating an entirely new slice and copying many more elements into it.

That is why this is a more efficient way to prepend elements. But remember, as I said above, that prepending will always be slower than appending, so make sure that it’s absolutely necessary before you do, especially if you’re working with large amounts of data and concerned about performance.

Inserting an Element Into a Slice at an Arbitrary Index

We’ve seen how to add elements at the beginning or the end of a slice, so the next logical step is to discover how to add them everywhere in between. Look at the InsertIntoSliceAtIndex function below, which can place an element into a slice at a given index:

package main

func InsertIntoSliceAtIndex[T any](destination []T, element T, index int) []T {
	if len(destination) == index {
		return append(destination, element)
	}

	destination = append(destination[:index+1], destination[index:]...) // index < len(a)
	destination[index] = element

	return destination
}

You can see that we first perform a test to check whether the insertion index is one greater than the largest current index (in other words, equivalent to the length of the slice), in which case we simply use the append function to add the element. This is the easiest case to deal with.

If the index argument is lower than zero or greater than the length of the slice, the Go runtime will panic, because it’s only possible to insert into indices that the slice already has (with the single exception discussed in the last paragraph).

The next line after the conditional block contains a Go idiom for shifting all of the elements currently at or above the index up by one place. This, of course, makes room for the new element to be inserted at the index.

Finally, we return the destination slice, since we have modified it through the use of the append function. Any code that calls the function can then save that modified slice, overwriting the original one.

Note also that the InsertIntoSliceAtIndex function uses the new generic syntax introduced in Go 1.18. However, you can easily rewrite the function to use specific types, if you’re using an earlier version of Go.

package main

import (
	"fmt"
	"math"
)

func InsertIntoSliceAtIndex[T any](destination []T, element T, index int) []T

func main() {
	usefulNumbers := []float64{math.Phi, math.Pi}

	usefulNumbers = InsertIntoSliceAtIndex(usefulNumbers, math.E, 1)

	fmt.Println(usefulNumbers)
}

The code above gives an example of how to use the InsertIntoSliceAtIndex function that we defined.

Let’s say that I want to store my three favourite mathematical constants in a slice in ascending order from smallest to largest — but I’ve missed the one in the middle out!

In that case, I can simply pass the original usefulNumbers slice to the InsertIntoSliceAtIndex function, along with the new value and the index that I want it to be inserted at. If math.E is inserted at index one, that means that the math.Pi value will be moved up one place into index two, and math.Phi will remain at index zero, so the new value will be located exactly in the center.

How to Clone a Slice by Copying All the Elements

In some programming languages, where slices or arrays are copied by value, you could assign a slice to a new variable in order to obtain a clone, like so:

package main

import (
	"fmt"
	"strings"
)

func main() {
	evens := []int{2, 4, 6, 8}
	incorrectlyClonedEvens := evens

	fmt.Println(incorrectlyClonedEvens)

	evens[0] = 20

	fmt.Println(strings.Repeat("*", 10))
	fmt.Println(evens)
	fmt.Println(incorrectlyClonedEvens)
}

However, that doesn’t work in Go, because the new variable still points to the same area of memory as the old variable. So, in the example above, when we set an element to one of the slice’s indices using the incorrectlyClonedEvens variable, we will also be setting the same element to the same index of the original evens variable.

This can be seen in the output:

[2 4 6 8]
**********
[20 4 6 8]
[20 4 6 8]

If you want to clone a slice so that its elements are stored at an entirely new location in memory, you will need to initialize a new slice with the same length as the old slice and then use the copy builtin function to duplicate all of the elements:

package main

import (
	"fmt"
	"strings"
)

func main() {
	evens := []int{2, 4, 6, 8}
	clonedEvens := make([]int, len(evens))

	copy(clonedEvens, evens)

	fmt.Println(clonedEvens)

	evens[0] = 20

	fmt.Println(strings.Repeat("*", 10))
	fmt.Println(evens)
	fmt.Println(clonedEvens)
}

When we look at the output, we can now see that changing the original slice does not modify the cloned slice, showing that the two slices no longer use the same area of memory:

[2 4 6 8]
**********
[20 4 6 8]
[2 4 6 8]

Final Thoughts About Appending, Prepending and Inserting

When it comes to placing elements into a slice, the append function is king! That’s what you should always think about before you consider using any other approach. It’s the most idiomatic way to add elements to a slice in Go.

Inserting elements into the middle or prepending them at the beginning of a slice will always have a more significant performance cost, compared to appending them at the end.

A jigsaw puzzle with the last piece missing, ready to be inserted. The image is used as a visual metaphor for adding elements to a slice.
Adding an element to a slice should be no more difficult than adding the final piece to a jigsaw puzzle.

So try to think laterally, if you can: for example, if you need to keep the elements of a slice in a certain order, you may want to think about appending new elements and then sorting the slice, instead of inserting each element into the correct place initially.

However, there may well be cases when you do want — or need — to pay the cost of putting new elements into a slice at a position other than the end, which is why we have spent some time looking above at various ways to perform prepends and insertions.

Leave a Reply

Your email address will not be published.