Working with arrays and slices in Go

Arrays and Slices are two of the most common data structures you will ever use if you program in Go. They share a lot of properties as slices are ultimately build on top of arrays but also quite different.

Arrays in Go

Very briefly when Go refers to arrays, it refers to a continuous memory segment that holds elements of the same type. Arrays are defined either using the var declaration and then assigning values to the different positions of the array

var a [3]int // [0 0 0]
a[0] = 10	 // [10 0 0]

or using an array literal, which means using concrete values. We can provide the exact number of elements and then the values or we can use the special ... notation which instructs Go to count the elements for us.

b := [3]int{10, 20, 30}   // [10 20 30]
c := [...]int{10, 20, 30} // [10 20 30]

Slices in Go

Arrays can be a bit restrictive as we have to pre-define the numbers of elements that they contain which may not always be known to begin with. Go has another data structure called slice which in essence is a dynamic array. Slices are defined by 3 values: a pointer to an underlying array, the length of the slice (how many elements from the array it uses) and the capacity (how many elements can the slice contain – this depends on the size of the array).

A slice defined using make([]byte, 5) – from the Go blog

Fun facts about arrays & slices

These are a few fun facts about working with arrays and slices that it’s good to keep in mind as they can affect the behavior of your program.

#1: Arrays are passed as values, slices always contain a pointer to an array

Say that you write two functions, one that accepts an array as a parameter and one that accepts a slice.

func passAnArray(a [3]int) {
  a[0] += 100
}

func passASlice(a []int) {
  if len(a) == 0 {
    a = append(a, 0)
  }
  a[0] += 100
}

Arrays are always passed by value which means that a new copy of the array will be passed in the function and changes to the array are not reflected outside of your function. On the other hand slices include a pointer to an underlying array which means that modifications are visible outside of the function.

aa := [3]int{1, 2, 3} // 3 element array
ab := []int{1, 2, 3}  // 3 element slice

passAnArray(aa)
passASlice(ab)

fmt.Println("aa: ", aa) // [1 2 3]
fmt.Println("ab: ", ab) // [101 2 3]

If we run the above code the first print statement will print [1 2 3] as the array remains unchanged whereas the second one will return [101 2 3] as the slice got modified in the function. The only way to achieve the same result with an array and a function is to return the array from the function and then re-assign it to the variable.

#2: Slices are doubled in size once they outgrow their original capacity

This is an interesting one. Slices are dynamic arrays that can grow in size, but how does this really work?

We can define a new slice using Go’s make function that takes as arguments the type of slice, the initial length of the slice and the capacity (which is optional). And we can append elements to the slice using Go’s append function that takes as arguments the slice to append to and a number of elements to add to the slice. So let’s have a look at the example below

d := make([]int, 0)
fmt.Printf("%T %v %d %d \n", d, d, len(d), cap(d))
// this will throw an error, there is no position 0 yet
// d[0] = 10

d = make([]int, 5) // same as d = make([]int, 5, 5)
d[0] = 10          // [10 0 0 0 0]
fmt.Printf("%T %v %d %d \n", d, d, len(d), cap(d))

d = append(d, 1)
fmt.Printf("%T %v %d %d \n", d, d, len(d), cap(d))

d = append(d, 2, 3, 4, 5)
fmt.Printf("%T %v %d %d \n", d, d, len(d), cap(d))

d = append(d, 6)
fmt.Printf("%T %v %d %d \n", d, d, len(d), cap(d))

In the first line we define a slice of int of 0 size. This means that the slice has no elements in it at the moment.

The second example creates a new slice of length 5 which means that the underlying array has already been created, has 5 elements and they are all set to the zero value for the array, in this case 0. Because the elements already exist we can assign to them directly using d[0] = 10.

In the third example we append an element to the array which means that it will now have a length of 6 but a capacity of 10 🤔. Now this is interesting. Our append needed to add a new element but the underlying array was too small. Go had to extend the array, but it does not extend it 1 element at the time. Go always doubles the size of the array once it the slice has reached its capacity. And the same happens again on line 71 where Go will double the array again now making it have a capacity of 20. The output from all the fmt.Printf functions can be seen below

#3: You can’t sort an array but you can sort a slice

This took me by surprise when I first encountered it. Go has a package called sort. This package only sorts slices and user-defined collections. So let’s have a look how that works.

The easiest way to sort an a slice of integers is to simply call sort.Ints(s) passing in your slice. But say for some reason you don’t want to use the already defined function because it sorts in ascending order and you want to sort the array in descending order. Then you can use the sort.Slice function that allows us to define they way the elements of the slice are going to be compared in the ordering function.

	// sort in ascending order
	s := []int{5, 3, 7, 2, 4, 1, 6, 9, 8, 10}
	sort.Ints(s)
	fmt.Printf("%T %v\n", s, s)

	// sort in descending order
	s = []int{5, 3, 7, 2, 4, 1, 6, 9, 8, 10}
	sort.Slice(s, func(i, j int) bool {
		return s[i] > s[j]
	})
	fmt.Printf("%T %v\n", s, s)

The way sort works under the hood is by defining an interface that the data structures need to define. This interface has 3 methods, Len, Less and Swap that have already been defined for slices of ints in the same package.

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

So the way to actually sort an array is to use sort.Ints but pass in a slice pointing to the array that you want to sort 🤯.

	// using sort to sort an array
	a := [10]int{5, 3, 7, 2, 4, 1, 6, 9, 8, 10}
	sort.Ints(a[:])
	fmt.Printf("%T %v\n", a, a)

Final thoughts

If you haven’t already I would definitely recommend reading the Go slices: usage and internals post from the official Go Blog as it helps clarify a lot of the concepts mentioned in this post.

If you are interested to learn more about how the append function works on slices then check out this blog post Arrays, slices (and strings): The mechanics of append.

Leave a Reply