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 isO(n²); usestrings.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 costsO(k). - Shared memory means writes and appends can show up in other slices.
- String concatenation copies, so repeated
+=in a loop is quadratic; usestrings.Builder. append(a, b...)joins slices in time proportional to what it adds, plus a copy when it must grow.