Notes on Go's Slice Data Type

This post will mostly be helpful if you're new to slices in Go.

When it comes to understanding slices, Andrew Gerrand's post Go Slices: usage and internals is always brought up. It's very useful and I've referred to it myself multiple times. However, nothing helps more than practice. Instead of exlpaining in great detail what they are, I thought I'd share additional information that may be useful to others who may be new to slices or need a refresher.

First off, I'd like to mention a slice's implementation from runtime.h, which is in C:

struct  Slice
{                // must not move anything
    byte*   array; // actual data
    uintgo  len;   // number of elements
    uintgo  cap;   // allocated number of elements
};

Recall that a byte is an unsigned, 8 bit integer, while uintgo is an unsigned integer (32 or 64 depending on your architecture). The array field of a slice simply stores an address.

The len field will indicate the size of the current slice of an array.

cap indicates the size of the array that the array field points to.

Default Values

The value of an empty slice is nil while len() and cap() will return a value of 0.

Passing Slices By Name

One of the most fundamental details to remember is the concept of passing by reference and passing by value.

When a slice is passed by name, the three fields of a slice (the header) are the values that are copied from and assigned to.

Although the values of the header are passed by value, the value of the array field is an address. With that said, I contend:

The headers of a slice are passed by value. The underlying array that the array field points to is passed by reference.

What does this mean? Well, the data of the underlying array is passed by reference because the address is what is copied. When the underlying array is modified, the array field of a slice is obviously unchanged.

The remaining headers are not pointers, so their values must be updated when the slice is manipulated + there is a desire for the changes to persist. Consider this:

copying_slices.go (For brevity, output statements excluded below.)

package main

import "fmt"

func main() {  
        intSlice := []int{1, 2, 3}

        secondIntSlice := intSlice

        secondIntSlice[0] = 9

        secondIntSlice = secondIntSlice[0:1]
}
Output
intSlice:[1 2 3]

secondIntSlice[0] = 9.

secondIntSlice:[9 2 3]

intSlice:[9 2 3]

sliced secondIntSlice to [0,1].

secondIntSlice:[9]

intSlice - len:3 cap:3

secondIntSlice - len:1 cap:3  
Explanation

We create a slice intSlice which will refer to an integer array with three elements.

Then a second slice is declared, secondIntSlice, in which intSlice is assigned to it, copying the values of the headers over.

The program modifies secondIntSlice[0] and prints intSlice to demonstrate that their data fields are pointing to the same array.

secondIntSlice is then sliced to demonstrate its headers being updated.

In Memory

Here's a vague example of what things look like under the hood, the address is abitrary:

copying_slices_image.go

A Little More...

This blog post was inspired by a Gopher on /r/golang who was having some trouble with slices. This person had wanted to write a function that would "fill out" a struct, which had a field of type slice, when one was passed to it. Here's the post.

I've been trying to pass a slice of a struct to a function in order to get filled with contents. ... I'm not pasing it with pointers, just like a plain variable... I return to the caller function but the array is still empty, even when data was added at the called function.

To replicate this, here's use_case_problem.go (For brevity, output statements excluded below.):

package main

import "fmt"

type Foo struct {  
    intSlice []int
}

func Fill(f Foo, newValue int) {  
    f.intSlice = append(f.intSlice, newValue)
}

func main() {  
    var customType Foo

    Fill(FooVariable, 12)
}
OUTPUT
main - FooVariable.intSlice:[]  
main - Executing Fill(FooVariable, 12)  
Fill - Executing append(f.intSlice, newValue)  
Fill - f.intSlice:[12]  
main - FooVariable.intSlice:[]  

As you can see, FooVariable is empty in main before and after the Fill function was called.

In the Fill function, the append function takes care of allocating space for an integer (if there isn't space already) and assigns the value of the integer variable newValue to the particular element.

append returns a slice in which its header values are then assigned to the local variable f.

The newly appended value of 12 is present since f's header values were updated append, yet isn't present in FooVariable because its headers never change.

The solution? Update the headers.

One way of achieving the desired functionality is to create a function that will return a slice. An assignment could be made to a slice from the returned value, effectively updating the slice headers. This is how the append() function works. For example:

slice_example.go

package main

import "fmt"

func intSliceHandler(intSlice []int, newValue int) []int {  
        return append(intSlice, newValue)
}

func main() {  
        var intSlice []int

        fmt.Printf("\nmain - intSlice:%v\n", intSlice)

        intSlice = intSliceHandler(intSlice, 5)

        fmt.Printf("\nmain - intSlice after intSliceHandler():%v\n\n", intSlice)
}
OUTPUT
main - intSlice:[]

main - intSlice after intSliceHandler():[5]  

Another method of populating a slice would be to preallocate the space and simply have a function populate the next element with the new value. Again, the header of the slice (len field) would need to be updated to reflect the new addition.

Thoughts and Other Comments

  • Slices are tricky, but with practice you'll be fine.

  • Returning the address of a local variable is fine. The space will persist.