Robin Moffatt
Posted on June 29, 2020
Slices made sense, until I got to Slice length and capacity. Two bits puzzled me in this code:
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// len=6 cap=6 [2 3 5 7 11 13]
// --
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// len=0 cap=6 []
// --
// Extend its length.
s = s[:4]
printSlice(s)
// len=4 cap=6 [2 3 5 7]
// --
// Drop its first two values.
s = s[2:]
printSlice(s)
// len=2 cap=4 [5 7]
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
First up, and again this is my non-coding background coming through, but if s
starts off as something
s := []int{2, 3, 5, 7, 11, 13}
// [2 3 5 7 11 13]
and then we change it to something else
s = s[:0]
// []
how can it revert to something based on its previous incarnation?
s = s[:4]
// [2 3 5 7]
Which eventually made sense to me once it was explained that because s
is a slice, it is a pointer to the underlying array. This is explained here, and so
s := []int{2, 3, 5, 7, 11, 13}
is building an array and declaring a slice on it in the same statement. It’s a more concise way of doing something like this:
myArray := [6]int{2, 3, 5, 7, 11, 13}
s := myArray[:]
When we appear to reassign s
to a new value
s = s[:0]
It’s actually declaring s
as a slice as a before, based on the pointer against the original array. We can infer this from the fact that the capacity of the slice remains as 6
s = s[:0]
// len=0 cap=6 []
and thus when we extend it, it’s still against the original array that we were pointing to:
s = s[:4]
// len=4 cap=6 [2 3 5 7]
So s
is a slice on top of the same array each time, just with a different definition (thus the length changes, not the capacity).
The second bit that puzzled me was, given the above explanation of s
being a pointer to the same array, how can resizing it down and then up still retain the values and capacity…
s = s[:0]
// len=0 cap=6 []
s = s[:4]
// len=4 cap=6 [2 3 5 7]
whilst also resizing it down and up not retain the values and capacity…
s = s[2:]
// len=2 cap=4 [5 7]
s = s[0:4]
// len=4 cap=4 [5 7 11 13]
The answer is related to the first point above - pointers. When we declare the slice and increase the lower bound ([2:]
) we’re actually moving the offset of the pointer against the underlying array. Any subsequent references are now based on the pointer to this offset, and not the original one.
Here’s another example that I worked through to help figure it out:
(try it on Go playground)
package main
import (
"fmt"
)
func main() {
myArray := [6]int{2, 3, 5, 7, 11, 13}
// y is a slice on myArray
// With no bounds specified it defaults to the lowest (zero) and
// highest (five) of the array
// There are six entries (len=6) and the array that it points to
// has six entries (cap=6)
y := myArray[:]
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 6 cap 6 value [2 3 5 7 11 13]
// myArray len 6 cap 6 value [2 3 5 7 11 13]
// y is a slice against the same array that y *pointed to* previously
// This time we take the first four entries (len=4). The slice is still
// pointing to the same original array which has six entries (cap=6)
y = y[0:4]
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 4 cap 6 value [2 3 5 7]
// myArray len 6 cap 6 value [2 3 5 7 11 13]
// y is a slice against the same array that y *pointed to* previously
// This time we take no entries (len=0). The slice is still
// pointing to the same original array which has six entries (cap=6)
y = y[:0]
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 0 cap 6 value []
// myArray len 6 cap 6 value [2 3 5 7 11 13]
// Now we do something different from the above pattern. We shift the
// point to which y points, and now it starts from the fifth position
// of the underlying array (it's zero based so fifth position=4).
// There are two entries (five and six) so len=2, and because we're now
// actually pointing to the array starting at the second entry the capacity
// changes too (cap=2)
y = y[4:6]
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 2 cap 2 value [11 13]
// myArray len 6 cap 6 value [2 3 5 7 11 13]
// Now that we've shifted the pointer to a different offset in the source array
// our bounds have different references.
// This refers to the second position (zero based, so 1) in the array but starting
// from the redefined start offset that we created in the above slice
y = y[1:2]
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 1 cap 1 value [13]
// myArray len 6 cap 6 value [2 3 5 7 11 13]
// Since the slice is just a pointer to the underlying array we can change the array and
// the slice will reflect this
myArray[5]=100
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 1 cap 1 value [100]
// myArray len 6 cap 6 value [2 3 5 7 11 100]
// Conversely, changing the slice value reflects in the array too
y[0]=200
fmt.Printf("y len %d\tcap %d\tvalue %v\n", len(y), cap(y),y)
fmt.Printf("myArray len %d\tcap %d\tvalue %v\n\n", len(myArray), cap(myArray),myArray)
// y len 1 cap 1 value [200]
// myArray len 6 cap 6 value [2 3 5 7 11 200]
}
This blog post goes into some lower-level stuff around Slices that was very useful. A concept it uses that I’d not come across yet was the underscore, which is explained well in this StackOverflow answer (and then gets covered soon after in the Tour [here).
Other references that were useful:
https://stackoverflow.com/questions/50713681/extend-the-length-and-keep-the-value
https://stackoverflow.com/questions/43294449/decreasing-slice-capacity
https://stackoverflow.com/questions/47256103/golang-slice-variable-assign-from-tutorial
Appending to a slice - why doesn’t the capacity match the length?
👉 A Tour of Go : Appending to a slice
This all made sense, except for when I noticed the cap
(6) wasn’t in line with the len
(5) in the final example.
func main() {
var s []int
// len=0 cap=0 []
s = append(s, 0)
// len=1 cap=1 [0]
s = append(s, 1)
// len=2 cap=2 [0 1]
s = append(s, 2, 3, 4)
// len=5 cap=6 [0 1 2 3 4]
}
Poking around a bit more with this I saw that the capacity doubled each time it needed to be increased:
package main
import "fmt"
func main() {
var s []int
for i:=0;i<20; i++ {
s = append(s,i)
printSlice(s)
}
}
func printSlice(s []int) {
fmt.Printf("len=%d \tcap=%d \n", len(s), cap(s))
}
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=4 cap=4
len=5 cap=8
len=6 cap=8
len=7 cap=8
len=8 cap=8
len=9 cap=16
len=10 cap=16
len=11 cap=16
len=12 cap=16
len=13 cap=16
len=14 cap=16
len=15 cap=16
len=16 cap=16
len=17 cap=32
len=18 cap=32
len=19 cap=32
len=20 cap=32
This is discussed in this StackOverflow answer.
Exercise: Slices
👉 https://tour.golang.org/moretypes/18 [A Tour of Go : Exercise: Slices]
This dropped me in at the fairly deep end, and I only just kept my head above water ;-)
I went back to previous examples, particularly Creating a slice with make and Slices of slices, but I couldn’t figure out how to combine the two concepts. This kind of thing didn’t work
p := make([]make([]uint8,dx),dy)
I’d have liked to see a hints or work answer for the exercise, but with the power of Google it was easy enough to find a few :) These answers got me on the right tracks to first create the slice and then create within it iteratively the additional slice (which to be fair the exercise text does specify, with hindsight)
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
p := make([][]uint8,dy)
for i := range p {
p[i] = make([]uint8,dx)
}
return p
}
func main() {
pic.Show(Pic)
}
When you run this you get a nice blue square. Now to add some pattern to it.
Just to experiment with what was going on I tried something, anything … :)
for y := range p {
for x := range p[y] {
p[y][x]=(uint8(x)+uint8(y))
}
}
Casting uint8
was necessary (and is mentioned as a hint in the exercise text) because otherwise it fails with ./prog.go:14:11: cannot use x + y (type int) as type uint8 in assignment
- I thought that this would work, to declare the variable types first, but it didn’t and threw the same error.
var x,y uint8
for y := range p {
for x := range p[y] {
p[y][x]=(x+y)
}
}
Other patterns:
for y := range p {
for x := range p[y] {
p[y][x]=(uint8(x)*uint8(y))
}
}
Posted on June 29, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.