Start learning free

GO · BIG O NOTATION

Time Complexity Practice 2 in Go, explained

Slicing a slice is cheap

In many languages, taking part of a list copies it. In Go, nums[a:b] builds a new slice header that points into the same array, with a new start and length. No element is copied, so slicing is O(1), whatever the size of the part.

a := []int{1, 2, 3, 4, 5}
b := a[1:4]
b[0] = 99
fmt.Println(a, b, len(b), cap(b))
[1 99 3 4 5] [99 3 4] 3 4

Writing to b[0] changed a, because both slices see the same memory. The capacity of b is 4: it can grow up to the end of the original array. This leads to a surprise with append:

c := append(b, 100)
fmt.Println(a, c)
[1 99 3 4 100] [99 3 4 100]

There was room in the shared array, so append wrote 100 into it and overwrote a[4]. A full slice expression, a[low:high:max], limits the capacity so that the next append has to allocate a new array.

Copying when you need independence

To get a slice that doesn't share memory, copy it. slices.Clone(a[1:4]) or copy into a slice made with make cost O(k) for k elements, and the result is independent:

d := slices.Clone(a[1:4])
d[0] = -1
fmt.Println(a, d)
[1 99 3 4 100] [-1 3 4]

Sharing has one more cost: a small slice taken from a huge array keeps the whole array alive in memory. Cloning the small part lets the large array be freed.

Slicing a string is cheap too

For strings, s[a:b] is also O(1). Strings are immutable, so sharing the bytes is safe, and nobody can change them through the new string:

s := "hello world"
t := s[6:]
fmt.Println(t, len(t))
world 5

The indices are byte positions, so slicing a string in the middle of a multi-byte character produces invalid text.

Concatenation copies

Joining two strings with + creates a new string and copies the bytes of both, so s + t costs O(len(s) + len(t)). This looks harmless until it happens in a loop:

s := ""
for i := 0; i < 5; i++ {
    s += "ab"
}
fmt.Println(s)
ababababab

Each pass copies the whole string built so far. With n passes adding a fixed amount, the total copying is 1 + 2 + ... + n steps, which is O(n²). For short strings it doesn't matter, but for large ones it does.

The fix is strings.Builder, which keeps a growing buffer and appends to it:

var sb strings.Builder
for i := 0; i < 3; i++ {
    sb.WriteString("ab")
}
fmt.Println(sb.String())
ababab

The total work is O(n), like append on a slice. When the pieces are already in a slice of strings, strings.Join(parts, "") does the same in one call.

Concatenating slices

Go has no + for slices. You join them with append(a, b...), which costs O(len(b)) when a has spare capacity and O(len(a) + len(b)) when it has to allocate a new array. Adding at the front, as in append([]int{0}, q...), always copies all of q.

a := []int{1, 2}
b := []int{3, 4}
fmt.Println(append(a, b...))
[1 2 3 4]

Because the result may share memory with a, two appends to the same slice can overwrite each other. With x := make([]int, 2, 10), both y := append(x, 1) and z := append(x, 2) write into the same spare slot, and afterwards both y and z print as [0 0 2].

Common mistakes

  • Treating a slice expression as a copy. Changes through one slice show up in the other.
  • Calling slicing O(n) and avoiding it. Taking sub-slices in a recursive algorithm doesn't copy data; passing indices is an alternative, not a necessity.
  • Building a string with += in a long loop. It is O(n²); use strings.Builder.
  • Prepending in a loop. Each prepend is O(n). Append and reverse once at the end, or build the result backwards.

Key takeaways

  • Slicing a slice or a string is O(1) because it shares memory; copying a part costs O(k).
  • Shared memory means writes and appends can show up in other slices.
  • String concatenation copies, so repeated += in a loop is quadratic; use strings.Builder.
  • append(a, b...) joins slices in time proportional to what it adds, plus a copy when it must grow.
Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free