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
.