Arrays and slices
The data structure you use all day, and the shared-memory trap inside it.
- Explain the difference between a slice's length and its capacity.
- Predict the output when you write through a sub-slice.
- Fix a shared-memory bug with
copyor a full slice expressions[a:b:b].
What is the difference between an array and a slice?
An array has a fixed length that is part of its type; a slice is a resizable view over an array, and it is what you use day to day. In Go an array’s length is part of its type. [3]int and [4]int are different types, and passing an array to a function copies the whole thing. That is almost never what you want, so arrays mostly exist to be the thing a slice points at.
var arr [3]int = [3]int{1, 2, 3}// arr can never hold four elementsWhat is a slice made of?
A slice is a small three-field value: a pointer to a backing array, a length, and a capacity. Length is how many elements you can index. Capacity is how many fit before Go has to allocate a bigger array.
s := []int{1, 2, 3}s = append(s, 4) // note: append RETURNS the new slicefmt.Println(len(s), cap(s))s2 := make([]int, 0, 10) // length 0, room for 10 — no reallocation yetsub := s[1:3] // elements 1 and 2append may allocate a new array and copy, so it returns a slice. You must write s = append(s, x). A bare append(s, x) with the result unused does not compile. t := append(s, x) followed by more work on s does compile, and is the version that bites.Why does changing one slice change another?
Because slicing shares memory. A sub-slice does not copy. It points into the same backing array. Change one, and you have changed the other.
data := []int{1, 2, 3, 4, 5}head := data[:2]head[0] = 99fmt.Println(data) // [99 2 3 4 5] — data changed too// When you want independence, copycp := make([]int, len(data))copy(cp, data)cp[0] = 1 // data is untouchedHow do you stop append from overwriting the original? (s[a:b:b])
Cap the capacity with a full slice expression, s[a:b:b], so the next append has to copy. A sub-slice usually keeps the spare capacity of the array behind it. So append on the sub-slice can write straight over elements the original still uses. A full slice expression s[low:high:max] sets the capacity to max - low. Write s[a:b:b] and capacity equals length, so the next append has no room and copies to a new array instead of overwriting.
data := []int{1, 2, 3, 4, 5}a := data[0:2] // len 2, cap 5 — spare room is data's elementsa = append(a, 99)fmt.Println(data) // [1 2 99 4 5] — append overwrote data[2]!data = []int{1, 2, 3, 4, 5}b := data[0:2:2] // len 2, cap 2 — no spare roomb = append(b, 99) // must copy to a new arrayfmt.Println(data) // [1 2 3 4 5] — untouchedfmt.Println(b) // [1 2 99]Try it yourself
package mainimport "fmt"func main() { data := []int{1, 2, 3, 4, 5} head := data[:2] head[0] = 99 fmt.Println("data after writing to the sub-slice:", data) // 99 is there too cp := make([]int, len(data)) copy(cp, data) cp[0] = 1 fmt.Println("after copy, data is untouched:", data) safe := data[0:2:2] safe = append(safe, 7) fmt.Println("capped append left data alone:", data, safe)}- An array’s length is part of its type. You will rarely use one directly.
- A slice is a pointer, a length and a capacity.
appendreturns a new slice, so always reassign. - Sub-slices share the backing array, so a write through one is visible through the other.
copywhen you need independence.s[a:b:b]caps the capacity so the next append reallocates.
copy and prove the original is untouched.Reversed(s []int) []int. It returns a NEW slice with the elements in reverse order and must not change s. Example: Reversed([]int{1, 2, 3}) → [3 2 1], and the input is still [1 2 3].Tab indents. Press Esc, then Tab, to leave the editor.
- 1Append to a slice inside a function without returning it, and watch the caller see nothing.
- 2Call
appendon anilslice and confirm it works. - 3Preallocate with
make([]T, 0, n)and printlenandcapas you append.