Slices, Maps and the Aliasing That Surprises You
Go has arrays, slices and maps. You will use slices and maps; the array exists mainly so that slices have something to point at, and understanding that is what explains every slice surprise.
package main
import "fmt"
func main() {
// An array has a FIXED length, and the length is part of its type.
var array [3]int
array[0] = 1
fmt.Printf("%T %v\n", array, array)
// A slice is a view onto an array: a pointer, a length and a capacity.
slice := []int{1, 2, 3}
fmt.Printf("%T len=%d cap=%d %v\n", slice, len(slice), cap(slice), slice)
// Assigning an array COPIES it. Assigning a slice shares the backing array.
other := array
other[0] = 99
fmt.Println("array:", array, "other:", other)
alias := slice
alias[0] = 99
fmt.Println("slice:", slice, "alias:", alias)
}
append, and the copy you did not ask for
append returns a slice. If the backing array has spare capacity it writes in
place; if not, it allocates a bigger array and copies. Both behaviours are
correct, and the difference is visible when two slices share an array.
package main
import "fmt"
func main() {
base := make([]int, 3, 5) // len 3, cap 5
fmt.Printf("len=%d cap=%d %v\n", len(base), cap(base), base)
// Room to spare: append writes into the SAME array.
grown := append(base, 4)
grown[0] = 99
fmt.Println("base:", base, "grown:", grown, "- they share an array")
// No room: append allocates a new one, and they stop sharing.
full := []int{1, 2, 3}
fmt.Printf("before: len=%d cap=%d\n", len(full), cap(full))
extended := append(full, 4)
extended[0] = 99
fmt.Println("full:", full, "extended:", extended, "- separate arrays now")
// Which is why you always assign the result back:
full = append(full, 5)
fmt.Println(full)
// A slice of a slice shares memory too, and holds the WHOLE array alive.
big := make([]byte, 1_000_000)
small := big[:10]
fmt.Println(len(small), cap(small), "- the megabyte cannot be collected")
independent := make([]byte, 10)
copy(independent, big[:10]) // copy is how you break the link
fmt.Println(len(independent), cap(independent))
}
Slicing, and the three-index form
package main
import "fmt"
func main() {
letters := []string{"a", "b", "c", "d", "e"}
fmt.Println(letters[1:3]) // b c - the end is exclusive
fmt.Println(letters[:2])
fmt.Println(letters[3:])
fmt.Println(letters[:])
// The capacity of a slice runs to the end of the backing array, so an
// append can overwrite what comes after.
head := letters[:2]
fmt.Printf("len=%d cap=%d\n", len(head), cap(head))
head = append(head, "OVERWRITTEN")
fmt.Println(letters)
// The three-index form caps it, so append must allocate instead.
fresh := []string{"a", "b", "c", "d", "e"}
safe := fresh[:2:2]
safe = append(safe, "appended")
fmt.Println(fresh, safe)
}
Maps
package main
import (
"fmt"
"sort"
)
func main() {
stock := map[string]int{"apples": 3, "pears": 0}
// Reading a missing key gives the zero value, not an error.
fmt.Println(stock["apples"], stock["plums"])
// The comma-ok idiom distinguishes "absent" from "present and zero".
if count, ok := stock["pears"]; ok {
fmt.Println("pears present, count", count)
}
if _, ok := stock["plums"]; !ok {
fmt.Println("plums absent")
}
stock["plums"] = 7
delete(stock, "pears")
fmt.Println(len(stock))
// Iteration order is RANDOMISED, deliberately - so nobody can depend on it.
keys := make([]string, 0, len(stock))
for key := range stock {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf(" %s=%d\n", key, stock[key])
}
// A map value is not addressable: you cannot modify a struct field in place.
type counter struct{ n int }
counters := map[string]counter{"a": {1}}
// counters["a"].n++ // will not compile
c := counters["a"]
c.n++
counters["a"] = c
fmt.Println(counters)
// A map of pointers avoids that, at the cost of nil handling.
pointers := map[string]*counter{"a": {1}}
pointers["a"].n++
fmt.Println(pointers["a"].n)
}
Passing slices and maps to functions
A slice header is copied, but the array it points at is not - so a function can change the elements but cannot change the caller's length. A map is a reference, so a function can add to it.
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3}
mutate(numbers)
fmt.Println("after mutate:", numbers) // changed
appendTo(numbers)
fmt.Println("after appendTo:", numbers) // NOT changed
numbers = appendAndReturn(numbers)
fmt.Println("after returning:", numbers) // changed, because it was returned
stock := map[string]int{}
addTo(stock)
fmt.Println("map:", stock) // changed: maps are references
}
func mutate(values []int) { values[0] = 99 }
func appendTo(values []int) { values = append(values, 4); _ = values }
func appendAndReturn(values []int) []int { return append(values, 4) }
func addTo(m map[string]int) { m["added"] = 1 }
What to take away
- An array is a value and copies; a slice is a view and shares.
appendmay write in place or reallocate - always assign the result back.- A small slice of a big array keeps the whole array alive;
copybreaks the link. s[:2:2]caps the capacity so an append cannot overwrite what follows.- Reading a missing map key gives the zero value;
v, ok := m[k]tells you which. - Map iteration order is deliberately random - sort the keys when order matters.