Skip to content
Theory · Arrays and slices · lesson 6 of 10

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:

GO
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:

  1. [20 30] 2 is the slice t and its length.
  2. [10 20] [40 50] is the start and the end of the slice s.

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:

  • 1 is the index of the first element, and it is included;
  • 3 is 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:

GO
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] = 9 inside 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.

LiveA slice of a slice and the shared arraylen(b) = 2 · cap(b) = 3
a
b := a[1:3]

b := a[1:3] — a window over the same array as a.

Watch which cells of a change

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:

  • 1 is the start, as before;
  • 3 is the end of the length: the length is 3 - 1, that is 2;
  • the third index, 3, is the end of the capacity: the capacity is also 3 - 1, that is 2.

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]:

  1. b is [2 3] with capacity 2, so it has no free room.
  2. b = append(b, 9) copies the elements into a new array and adds 9: b becomes [2 3 9].
  3. Nobody touched the original array, so a is still [1 2 3 4].
  4. From now on b has its own row of lockers, and writing b[0] = 7 no longer reaches a.

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 src into dst;
  • it returns the number of elements copied;
  • dst has its own backing array, so a write to it is not visible through src.
GO
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:

GO
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 index i to j - 1, with length j - i.
  • A slice of a slice shares the backing array with the original: a write through one is visible through the other.
  • append to 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 copy into 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.

Sign in another way