Slicing a slice
In earlier lessons a slice was a window onto a row of lockers. Within one window you can mark out a smaller window that looks at part of the same lockers.
A function often needs not the whole sequence but a part of it: the first three results, or everything except the first element. Copying the needed elements into a new slice one by one is slow. The expression s[i:j] takes a part of a slice at once:
package main
import "fmt"
func main() {
s := []int{10, 20, 30, 40, 50}
t := s[1:3]
fmt.Println(t, len(t)) // [20 30] 2
fmt.Println(s[:2], s[3:]) // [10 20] [40 50]
}The program prints two lines:
[20 30] 2is the slicetand its length.[10 20] [40 50]is the start and the end of the slices.
Bounds
A slice expression has three forms. Let's look at each.
Both indexes: s[i:j]
The expression s[1:3] has two bounds:
1is the index of the first element, and it is included;3is the index the slice ends before, and it is excluded.
So t has two elements, and the slice's length is 3 - 1, that is 2. If both indexes are equal, the slice is empty: s[2:2] prints as [].
Going out of bounds causes a panic. For the slice []int{1, 2, 3, 4, 5} the expression s[2:9] stops the program: slice bounds out of range [:9] with capacity 5.
No start: s[:j]
An omitted start means 0. So s[:2] is the first two elements, [10 20].
No end: s[i:]
An omitted end means len(s). So s[3:] is everything from index 3 on, that is [40 50].
The same syntax works for an array too: a[1:3] returns a slice.
A shared array
A slice of a slice does not copy elements and is created fast at any length. It points to the same backing array as s, only from a different place. Two windows look at the same lockers, so a write through one slice is visible through the other:
a := []int{1, 2, 3, 4}
b := a[2:]
b[0] = 7
fmt.Println(a, b) // [1 2 7 4] [7 4]The element b[0] and the element a[2] are the same memory cell. So the program prints [1 2 7 4] [7 4].
A slice passed to a function behaves the same way:
- the parameter gets a copy of the array reference, the length and the capacity;
- the array itself stays shared;
- so the write
s[0] = 9inside the function is visible to the caller.
Capacity and append
Interviewers like to ask this, and it hides a trap with append. The capacity of a slice of a slice counts from its start to the end of the original slice's capacity. The window can widen to the right up to the end of the row. What append does depends on how many indexes the expression has.
Two indexes: a[1:3]
For a := []int{1, 2, 3, 4}, b := a[1:3] has length 2 and capacity 3. So b = append(b, 9) creates no new array; it writes 9 into the locker a[3]. After that a becomes [1 2 3 9].
Press the button and watch which cells of a change after a write through b and after append.
b := a[1:3] — a window over the same array as a.
Three indexes: a[1:3:3]
Capacity is limited with a third index: a[1:3:3] is a full slice expression. Let's take it apart:
1is the start, as before;3is the end of the length: the length is3 - 1, that is2;- the third index,
3, is the end of the capacity: the capacity is also3 - 1, that is2.
The window can no longer be widened to the right: locker a[3] is outside its frame. Let's see what append does with b := a[1:3:3]:
bis[2 3]with capacity2, so it has no free room.b = append(b, 9)copies the elements into a new array and adds9:bbecomes[2 3 9].- Nobody touched the original array, so
ais still[1 2 3 4]. - From now on
bhas its own row of lockers, and writingb[0] = 7no longer reachesa.
This trick helps when you hand part of a slice to other code. If that code calls append, your elements stay intact. The third index cannot be greater than the capacity of the original slice, or the program stops with a panic.
An independent copy
An independent copy is made by the built-in copy(dst, src) function:
- it copies elements from
srcintodst; - it returns the number of elements copied;
dsthas its own backing array, so a write to it is not visible throughsrc.
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
dst[0] = 9
fmt.Println(n, src, dst) // 3 [1 2 3] [9 2 3]This is where the window analogy ends: in real life you move things from the lockers to a new row by hand. copy does it for you, but only into lockers that are already in the dst window.
Common mistake
The copy is made into an empty slice:
src := []int{1, 2, 3}
var dst []int
fmt.Println(copy(dst, src), dst) // ✗The program prints 0 []. The copy function does not add elements; it copies as many elements as the shorter of the two slices has. The length of dst is zero, so nothing is copied. Create dst with make([]int, len(src)), as above.
Key points
s[i:j]is the elements from indexitoj - 1, with lengthj - i.- A slice of a slice shares the backing array with the original: a write through one is visible through the other.
appendto a slice of a slice overwrites the original's elements while capacity allows;s[i:j:j]limits the capacity.- An independent copy is made by
copyinto a slice of the right length.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.