Slice Internals in Golang

A pineapple sliced horizontally

Courtesy of: Nik

Slice in Go: A Dynamic Collection

Slice in Go is analogous to C++ "vector". It is a contiguous collection of elements of the same type, that when it runs out of space[1] at runtime, it is dynamically exapanded to accomodate new elements.

Let's start with a contrived example:

// main.go

type Field string

type Fields []Field

func (f Fields) Add(field string) {
    f = append(f, Field(field))

    fmt.Printf("Add method, contents: %q, len: %d\n", f, len(f))
}

func main() {
    f := make(Fields, 0, 2)
    f.Add("first")

    fmt.Printf("main function, contents: %q, len: %d\n", f, len(f))
}

In this example, we create a type Fields, which is a collection of Field, and then add a method to the type to facilitate appending to the collection. Here's the output of the program:

Add method, contents: ["first"], len: 1
main function, contents: [], len: 0

What has just happened? Why is the ouput printed in the main function different from the one in the Add method?

To better understand what's happening under the hood, let's explore the internals of slices in Go.

Slice Internals

Slices in Go are a three word data structure, with each word containing either 4 or 8 bytes, depending on the CPU architecture. The first word represents a pointer to the backing array that supports the slice. The second and third words store the length and capacity of the supporting array, respectively.

slice header

Since Go is a "pass by value" language—meaning data is always copied regardless of its semantic nature—what happens in the example we saw earlier is that a copy of the slice header is passed as the receiver to the Add method.

As a result, when appending to the slice, we are modifying a copy of the slice header which has no effect on the original slice created in the main function.

However, it is important to note that the copy of the slice shares a pointer to the same backing array. This means the array gets modified, but the changes are not reflected in the length and capacity of the original slice. Therefore, the original slice will not see the changes[2].

Notes

  1. When length of a slice reaches its capacity, the slice will be reallocated to make room for new elements.
  2. As a reminder, we can only read up to(but not including) the index defined by the slice's length.