What does the third index in Go's full slice expression s[a:b:c] control, and why use it?
answer
- three numbers, not two
- the last one is not an end index
- capacity becomes c minus a
- re-slicing otherwise keeps the old capacity
- forces the next append to copy
basics
~20 sThe third index sets capacity: s[a:b:c] has length b-a and capacity c-a. Capping capacity at the length forces any later append to allocate a fresh array, so it cannot overwrite elements the source still uses.
solid answer
~40 sA two-index slice expression keeps the source's capacity, so `s[a:b]` can still reach elements past `b`. The full slice expression `s[a:b:c]` sets capacity explicitly: length is `b-a`, capacity is `c-a`, and `c` may not exceed the source's capacity or you get a run-time panic. The idiom is `s[a:b:b]` — capacity equal to length, so the first `append` on the result has no spare room and must allocate a new array. That is how you hand a window of a shared buffer to a caller without risking that their append clobbers the bytes after it. What it does not do is release memory: the result still points into the same backing array, so the whole array stays reachable. Full slice expressions are allowed on slices, arrays and pointers to arrays, but not on strings.
code
go · 10 linesbuf := make([]byte, 0, 32)
buf = append(buf, "hello world"...)
risky := buf[0:5] // len 5, cap 32
safe := buf[0:5:5] // len 5, cap 5
fmt.Println(len(risky), cap(risky)) // prints: 5 32
fmt.Println(len(safe), cap(safe)) // prints: 5 5
safe = append(safe, '!') // no room: copies into a new array, buf untouchedgo deeper
Be ready to read the three-index form correctly: length comes from the first two indices, and the third one sets capacity, both measured from the starting index.
Explain why it exists — a two-index re-slice inherits the source's capacity, so a later append can write past the window into elements the source still uses.
Show where you would require it in review: any exported function returning a view into a buffer the package keeps, and be clear that it stops appends but not element writes and frees nothing.
Decide the package-boundary policy: which APIs may return views at all versus always copying, and what you are willing to pay in allocations for the safety that copying buys.
## Two indices versus three Writing `s[a:b]` produces a slice whose length is `b-a` — but whose capacity is whatever remains from index `a` to the end of the source's backing array. Re-slicing narrows the *view*, never the *reach*. That is why `append` on a re-sliced value can write into elements the original still exposes. The full slice expression adds a third index: `s[a:b:c]`. - length = `b - a` - capacity = `c - a` - the array pointer is unchanged: it still points at element `a` of the same array The indices must satisfy `0 <= a <= b <= c <= cap(s)`. Violating that is a run-time panic with a slice-bounds error (for a constant index on an array or string operand it is caught at compile time, but on a slice the capacity is not a compile-time constant). Note what the third index does *not* do: it does not change where the data lives, it does not copy anything, and it does not shrink the allocation. ## The reason it exists The motivating case is handing out part of a buffer you still own. ```go buf := make([]byte, 0, 1024) buf = append(buf, "hello world"...) word := buf[0:5] // len 5, cap 1024 -- dangerous ``` If a caller appends to `word`, there are 1019 bytes of spare capacity, so the append writes into `buf`'s array at index 5, silently corrupting the space character and anything you later write there. Both slices are correct-looking code; the collision is invisible at each site. Change it to `word := buf[0:5:5]` and the result has capacity 5. The caller's first append finds no room, allocates a fresh array, copies the five bytes into it and writes there instead. Your buffer is untouched. The cost is one allocation *only if the caller actually appends* — if they merely read, nothing extra happens. The same reasoning applies inside a package: a method that returns `p.items[:n]` invites the caller to append into `p.items`; returning `p.items[:n:n]` does not. ## What it protects and what it does not It protects **element identity**: nobody else's append can overwrite elements of the source array. It is a *write* barrier in the informal sense, expressed purely through capacity. It does **not** protect the elements the slice does expose. `word[0] = 'H'` still writes into the shared array, because index assignment does not consult capacity. If the caller must not mutate the data at all, capping capacity is not enough — you have to copy, or hand out a `string`, which is immutable. It also does **not** reduce memory. A slice keeps its entire backing array reachable regardless of length or capacity, so `buf[0:5:5]` still pins the whole 1024-byte array as long as it is alive. Capping capacity and freeing memory are different problems with different fixes; the fix for memory is to allocate a fresh, right-sized slice and `copy` into it. ## Where it shows up in practice - A library function returning a view of an internal buffer to a caller it does not control. - Splitting a buffer into non-overlapping regions that different goroutines or different call paths will each append to — without the cap, two regions can write to the same bytes. - Any API that documents "the returned slice is valid until the next call": capping capacity removes one class of surprise from it, though a copy removes all of them. ## Reading it correctly Because the syntax looks like three bounds, a common misreading is that `s[2:5:8]` yields a slice from 2 to 8 in some sense, or that the third number is an end index for the capacity in absolute terms *and* changes the length. It does not: length comes from the first two indices only. For `s := make([]int, 10, 100)`, `s[2:5:8]` has length 3 and capacity 6 — both measured from index 2. One syntactic restriction to remember: full slice expressions are defined for slices, arrays and pointers to arrays, but **not** for strings. Strings have no spare capacity to hand out in the first place, since they are immutable.
- Does capping a slice's capacity with s[0:n:n] reduce the memory the program holds?No. The slice still points into the same backing array, and a slice keeps its whole array reachable, so nothing is released. Capacity only decides whether an append can write in place. To actually let a large array be collected, allocate a right-sized slice with make and copy the elements you need into it.
- What happens if the third index exceeds the source slice's capacity?It is out of range and panics at run time with a slice-bounds error. The valid constraint is 0 <= a <= b <= c <= cap(s). Because a slice's capacity is not known at compile time, the compiler generally cannot catch it for you the way it can for a constant index into an array.
- Can you write a full slice expression on a string?No — full slice expressions are defined for slices, arrays and pointers to arrays only. A string supports the two-index form s[a:b], which yields another string sharing the same bytes. Since strings are immutable there is no append that could write through them, so there is nothing for a capacity cap to protect.
saying these in an interview costs you the question
- Says the third index is an end index that sets the length
- Claims capping capacity frees the rest of the array
- Believes it makes the returned elements read-only
- Thinks s[a:b] already has capacity b-a
- Expects a compile error rather than a panic when c exceeds cap