Slicing
So far you have built slices from scratch (literals and make) and grown them (append). The third way to get a slice is to take one from another slice, an operation called slicing. It is the source of both Go's most elegant patterns and one of its most surprising bugs (covered in the next lesson).
The basic slice expression
The syntax borrows from Python: s[low:high] returns a slice containing elements from index low up to but not including high. Both bounds are optional.
nums := []int{10, 20, 30, 40, 50}
fmt.Println(nums[1:4]) // [20 30 40]
fmt.Println(nums[:3]) // [10 20 30] (low defaults to 0)
fmt.Println(nums[2:]) // [30 40 50] (high defaults to len(nums))
fmt.Println(nums[:]) // [10 20 30 40 50] (whole slice)
This is a half-open interval: the low bound is included, the high bound is excluded. nums[1:4] gives you indices 1, 2, and 3 but not 4. The same convention shows up in the strings package, the sort package, and most Go APIs that take a range.
The length of the resulting slice is high - low. Out-of-range bounds still panic: nums[1:10] on a five-element slice crashes the program at runtime.
Slicing shares memory
Here is the important part. The result of a slice expression is not a copy. It points at the same underlying data as the original. A write through the new slice changes the element you would see through the old one too:
nums := []int{10, 20, 30, 40, 50}
middle := nums[1:4]
middle[0] = 999
fmt.Println(nums) // [10 999 30 40 50] (original changed!)
fmt.Println(middle) // [999 30 40]
middle[0] and nums[1] refer to the same memory location. Writing through one updates the other. That sharing is what makes slicing cheap. It is also what causes the aliasing bug in the next lesson, so keep it in mind.
Slicing and capacity
Slicing adjusts the start, the length, and the capacity of the view. Critically, cap of the result is measured from the slice's new start all the way to the end of the original backing array:
nums := make([]int, 5, 10) // original slice: len 5, cap 10
small := nums[1:3] // small: len 2 (indices 1 and 2), cap 9 (indices 1 through 9)
// nums -> [ 0 0 0 0 0 0 0 0 0 0 ]
// small -> ^ ^ (length = 2, from index 1 to index 3)
// ^ ^ ^ ^ ^ ^ ^ ^ ^
// (capacity = 9, from index 1 to end)
fmt.Println(len(small), cap(small)) // 2 9
small has length 2 (two elements visible) but capacity 9. That means the slice can see nine total positions starting from its new start. That matters for append: appending into small can overwrite positions that the original nums is still watching.
Let's see that in action.
nums := []int{10, 20, 30, 40, 50}
small := nums[1:3]
// small = [20, 30]
small = append(small, 99, 100)
// small = [20, 30, 99, 100]
// ^ ^
// (will overrite nums[3] and nums[4] from the original)
fmt.Println(nums) // [10 20 30 99 100]
fmt.Println(small) // [20 30 99 100]
small started with length 2 and capacity 4 (positions 1–4 of nums). The two append values had room inside the existing capacity, so Go wrote 99 into position 3 of the shared array and 100 into position 4. nums[3] and nums[4] changed, even though the code only touched small. Another variant of the aliasing theme, and the setup for the dedicated aliasing lesson that follows.
The starter declares words := []string{"alpha", "beta", "gamma", "delta", "epsilon"}. Print three slices of it, each on its own line:
words[1:3]should print[beta gamma]words[:2]should print[alpha beta]words[3:]should print[delta epsilon]
[beta gamma] [alpha beta] [delta epsilon]