At first, it’s easy to see arrays and slices as the same thing: a data structure to represent collections. However, they are actually quite different from one another.

In this post we will explore their differences and implementation in Go.

We will then look at some examples so that you can make a more informed decision on when to use them.

banner

Arrays

An array is a fixed collection of data. The emphasis here is on fixed, because once you set the length of an array, it cannot be changed.

Lets take an example of declaring an array of four integers:

arr := [4]int{3, 2, 5, 4}

Length and Type

The arr variable we defined in the above example is of type [4]int, which is an array of size 4. What’s important to note here, is that the 4 is included in the type definition.

What this means is that two arrays of different lengths are actually two separate types. You cannot equate arrays of different lengths, and you also cannot assign the value of one to the other:

longerArr := [5]int{5, 7, 1, 2, 0}

longerArr = arr
// This gives a compilation error

longerArr == arr
// This gives a compilation error

I found that a good way to think of arrays is in terms of structs. If we could construct the struct equivalent of arrays, it would probably look like this:

// Struct equivalent for an array of length 4
type int4 struct {
  e0 int
  e1 int
  e2 int
  e3 int
}

// Struct equivalent for an array of length 5
type int5 struct {
  e0 int
  e1 int
  e2 int
  e3 int
  e5 int
}

arr := int4{3, 2, 5, 4}
longerArr := int5{5, 7, 1, 2, 0}

It’s not recommended to actually do this, but it’s a good way to get an idea of how arrays of different lengths are different types altogether.

Memory Representation

An array is stored as a sequence of n blocks of the type specified:

array representation

This memory is allocated as soon as you initialize a variable of the array type.

Reference Passing

In Go, there is no such thing as passing by reference. Everything is passed by value. If you assign the value of an array to another variable, the entire value is copied.

array assignment

If you want to pass just the “reference” to the array, you can use pointers:

array pointers

In memory allocation, and in function, the array is actually a really simple data type, and works in much the same way as structs.

Slices

You can think of slices as an advanced implementation on top of arrays.

Slices were implemented in Go to cover some of the more common use cases faced by developers when working with collections, like dynamically changing their size.

Declaring a slice is almost the same as declaring an array, except that you must omit the length specifier:

slice := []int{4, 5, 3}

By looking at the code, slices and arrays look pretty similar, but there are actually significant differences in their implementation and uses.

Memory Representation

A slice is allocated differently from an array, and is actually a modified pointer. Each slice contains three pieces of information:

  1. The pointer to the sequence of data
  2. The length: which denotes the total number of elements currently contained.
  3. The capacity: which is the total number of memory locations provisioned.

slice memory representation

It follows then that slices of different lengths can be assigned each other’s values. Their type is the same, with the pointer, length, and capacity changing:

slice1 := []int{6, 1, 2}
slice2 := []int{9, 3}

// slices of any length can be assigned to other slice types
slice1 = slice2

A slice, unlike an array, does not allocate the memory of the data blocks during initialization. In fact, slices are initialized with the nil value.

Reference Passing

When you assign a slice to another variable, you still pass by value. The value here refers to just the pointer, length, and capacity, and not the memory occupied by the elements themselves.

slice assignment

Adding New Elements

To add elements to a slice, you normally use the append function.

nums := []int{8, 0}
nums = append(nums, 8)

Internally, this assigns the value specified to a new element, and returns a new slice. This new slice has it’s length increased by one.

slice appending element

If adding an element would need to increase the length beyond the available capacity, new capacity needs to be provisioned (the current capacity is normally doubled in this case).

This is why it’s often recommended to create a slice with the length and capacity specified beforehand (especially if you have a good idea of what it’s size might be). We can do this using the make function:

arr := make([]int, 0, 5)
// This creates a slice with length 0 and capacity 5
Can you figure out the output of this code?
package main

import (
	"fmt"
)

func main() {
	a := make([]int, 0, 10)
	b := a[8:10]
	fmt.Println(len(a), cap(a), len(b), cap(b))
}

When we initialize the slice a, we provision a capacity of 10 memory blocks. The slice itself is initialized without any elements, and so has a length of 0.

b is a slice of a, and so uses the same provisioned memory. Since we specify the slice to take the last 2 elements, b ends up having 2 zero value integers as its elements.

There are no remaining memory blocks provisioned after the last element of a original allocated memory, so the capacity of b remains at 2

a and b length and capacity

Should You Use Arrays or Slices?

Arrays and slices are quite different, and consequently, their use cases are quite different as well.

Let’s go through some examples from open source and the Go standard library, to see where they are used.

Case 1: UUIDs

UUIDs are 128-bit pieces of data that are often used to uniquely tag an object or entity. They are often represented in dash-separated hex values:

e39bdaf4-710d-42ea-a29b-58c368b0c53c

In Google’s UUID library, a UUID is represented as an array of 16 bytes:

type UUID [16]byte

This makes sense, since we know that a UUID is made out of 128 bits (16 bytes). We are not going to add or remove any bytes from a UUID, and so using an array to represent it makes much more sense that a slice.

Case 2: Sorting Integers

In this next example, we’re going to look at the sort.Ints function from the sort standard library:

s := []int{5, 2, 6, 3, 1, 4} // unsorted
sort.Ints(s)
fmt.Println(s)
// [1 2 3 4 5 6]

The sort.Ints function takes a slice of integers and sorts them in place. Slices are preferred here for two reasons:

  1. The number of integers is unspecified (there could be any number of integers to sort).
  2. The numbers need to be sorted in place. Using an array would pass the entire collection of integers as a value, and so the function would only sort it’s own copy, and not the one passed to it.

Conclusion

Now that we’ve covered the key differences between arrays and slices, and their use cases, here are some tips to decide which construct is more suitable:

  1. If the entity is described by a set of non-empty items of a fixed length: use arrays.
  2. When describing a general collection that you would add or remove elements from, use slices.
  3. If a collection can contain any number of elements, use slices.
  4. Will you be modifying the collection in some way? If yes, then use slices

As we can see, slices cover the majority of scenarios for creating an application in Go. Still, arrays do have their place, and are incredibly useful when the use case requires them.

Are there any use cases I missed? Are there any interesting use cases where you prefer arrays over slices (or vice-versa)? Let me know in the comments below 👇