Friday, May 28, 2021

Slices And Arrays In Go

I've been looking a little bit more at slices and arrays in Go and have come across a couple of things.

Firstly, I need to start thinking about slices as a subset of an array. From the Go docs:

The slice type is an abstraction built on top of Go's array type, and so to understand slices we must first understand arrays.

Secondly, I was struggling with the concept of capacity in slices and how updating one slice can update another.

Consider this example:

package main

import "fmt"

func appendOne(s []int) []int {
	return append(s, 1)
}

func main() {
	s1 := []int{0, 0, 0}
	s2 := s1
	fmt.Printf("Before appendOne:\n&s1: %p %[1]v\n&s2: %p %[2]v\n", s1, s2)
	s1 = appendOne(s1)
	fmt.Printf("After appendOne:\n&s1: %p %[1]v\n&s2: %p %[2]v\n", s1, s2)
	s1[0] = 2
	fmt.Printf("After update:\n&s1: %p %[1]v\n&s2: %p %[2]v\n", s1, s2)
}

I initialize a slice with 3 integers. I then assign the value of the newly created slice to another variable. They currently contain the same values and the same pointer.

Before appendOne:
&s1: 0xc00001c0f0 [0 0 0]
&s2: 0xc00001c0f0 [0 0 0]

We then call a function passing the first slice through as an argument and append a new value to it. We now have this as output:

After appendOne:
&s1: 0xc0000200f0 [0 0 0 1]
&s2: 0xc00001c0f0 [0 0 0]

The s1 pointer value has changed here as the capacity of the slice was increased to 4 items (from 3) and therefore a new underlying array was created.

Updating the s1 array only effects that array alone.

After update:
&s1: 0xc0000200f0 [2 0 0 1]
&s2: 0xc00001c0f0 [0 0 0]

Now consider this change:

func main() {
	s1 := make([]int, 4, 8)
}

The biggest change here is that the capacity is never exceeded even after appending therefore the pointer value doesn't change for s1.

After appendOne:
&s1: 0xc000014180 [0 0 0 0 1]
&s2: 0xc000014180 [0 0 0 0]

So when we try to update we get this:

After update:
&s1: 0xc000014180 [2 0 0 0 1]
&s2: 0xc000014180 [2 0 0 0]

Both slices are updated as they both point to the same value.

The same principle applies even if you pass a pointer as an argument instead of the slice itself.

func appendOne(s *[]int) {
	*s = append(*s, 1)
}

func main() {
	appendOne(&s1)
}

We still the same output:

After appendOne:
&s1: 0xc000014180 [0 0 0 0 1]
&s2: 0xc000014180 [0 0 0 0]
After update:
&s1: 0xc000014180 [2 0 0 0 1]
&s2: 0xc000014180 [2 0 0 0]

This example is also pretty interesting.

func main() {
	src := []int{}
	src = append(src, 0)
	src = append(src, 1)
	src = append(src, 2)
	fmt.Printf("%p %[1]v\n", src)
	dest1 := append(src, 3)
	fmt.Printf("%p %[1]v\n", dest1)
	dest2 := append(src, 4)
	fmt.Printf("%p %[1]v\n", dest2)
	fmt.Println(src, dest1, dest2)
}

The output we get is:

0xc0000180c0 [0 1 2]
0xc0000180c0 [0 1 2 3]
0xc0000180c0 [0 1 2 4]
[0 1 2] [0 1 2 4] [0 1 2 4]

This is confusing at first but the reason why dest1 and dest2 are the same is that they're relying on the same underlying array and pointer value coming from src.