Go Slice Interview Questions: Capacity, Aliasing, and append Surprises
Quick Overview
A slice copies its view, not its backing array; append behavior follows capacity guarantees rather than a guessed growth formula.
Go slice interview questions become easier when you track two things separately: the slice value and the backing array. Assignment copies the view. It does not copy every element. An append can write into storage that another slice already exposes, even when the receiving variable looks independent.
The useful interview habit is to draw the storage before predicting output. Record each view's starting position, length, and capacity, then follow the actual writes. That method also explains function boundaries, unexpected retention, and unsafe concurrent access. For the broader language context, see Golang Interview Questions for Backend Engineers.
Evidence boundary: Official facts below come from the Go specification and project documentation. Candidate reports: no verified report establishes that an employer asks these exact puzzles. Inference: the exercises, review criteria, and API recommendations are original practice material. Executable examples were checked with Go 1.27.1; their output does not depend on a particular capacity growth factor.

What does copying a slice actually copy?
Official fact: the Go slice specification describes a slice as a view of an underlying array, with a length and capacity. Assignment and argument passing copy the slice value. Multiple values can therefore refer to the same element storage.
For interview drawings, use a three-part model: starting address, visible length, available capacity. Treat it as a reasoning model, not an invitation to depend on an unsafe runtime layout. Two boxes representing slice variables can point into one longer row representing the array.
Length answers which element indices are currently valid. Capacity answers how far that view can extend through reslicing or reuse during append. Neither number tells you that the view exclusively owns its elements. A capacity of two can still describe storage shared with several other views.
Consider an API that accepts a batch of account IDs. If it sorts that slice in place, callers may observe reordered IDs through their own slice variables. The function did not receive a pointer to the caller's slice variable, but it still reached the same element storage. Our recommendation is to document whether the API borrows, mutates, or copies its input.
Can append change another slice without changing its length?
Predict all three printed slices before running this original exercise:
base := []int{10, 20, 30, 40}
a := base[:2]
b := base[1:3]
a = append(a, 99)
a[0] = 7
fmt.Println(base)
fmt.Println(a)
fmt.Println(b)
The output is [7 20 99 40], [7 20 99], and [20 99], on separate lines. The surprising operation is the append, which overwrites the third array slot. That slot is outside a's original length but inside its capacity and inside b's visible range.
Official fact: the append specification requires reuse when the existing capacity is sufficient and a new sufficiently large array otherwise. Here, a has length two and capacity four, so the appended element fits.
Use this trace to explain the result rather than saying that slices are “passed by reference.”
- Initial state: the array is
[10 20 30 40]. Viewastarts at index zero with length two and capacity four. - Append 99: the array becomes
[10 20 99 40]. The returned view assigned toahas length three. - Assign the first element:
a[0] = 7changes the same array to[7 20 99 40]. - Read the sibling view:
bstill starts at index one with length two, so it exposes[20 99].
Now change only the append to add three elements. That exceeds the available capacity, so a receives a new backing array. Predict that branch separately. Memorizing the first output without checking capacity will produce the wrong answer to the follow-up.
Does a full slice expression create an independent copy?
No. It can restrict append reuse through that view, but the existing elements remain shared. Official fact: a full slice expression controls capacity using its third index.
base := []int{10, 20, 30, 40}
head := base[:2:2]
head[0] = 11
out := append(head, 77)
out[1] = 88
fmt.Println(base)
fmt.Println(head)
fmt.Println(out)
The output is [11 20 30 40], [11 20], and [11 88 77]. The first mutation goes through the still-shared view. The positive-length append cannot fit in head, so out uses new storage. Mutating out[1] then leaves base[1] unchanged.
That gives an API designer a precise option: prevent a callee's append from overwriting elements beyond the provided window. It does not give the callee read-only access, and it does not isolate writes to elements already inside the window. If the callee promises not to mutate existing elements, say so separately.
A useful adversarial follow-up is to set head = head[:0] before appending. The slice still has capacity two, so the next append can reuse those slots. The earlier three-index expression did not permanently make the storage non-appendable. Its guarantee depends on the length and capacity at the later operation.

When do you need Clone instead of a capacity limit?
Official fact: slices.Clone copies elements by assignment, preserves nilness, and may return unused capacity. It is a shallow clone. For a nonempty slice of integers, the copied element storage lets you change an integer without changing the original slice.
Our exercise contrasts that useful isolation with a pointer-containing slice:
value := 5
original := []*int{&value}
copied := slices.Clone(original)
*copied[0] = 9
fmt.Println(*original[0])
other := 12
copied[0] = &other
fmt.Println(*original[0], *copied[0])
The output is 9, followed by 9 12. The first mutation follows a copied pointer to the same integer. The second replaces a pointer slot in the cloned slice, leaving the original slot alone. Distinguish copying the container from copying the reachable object graph.
Suppose a service returns a slice of records containing maps. Cloning the outer slice can protect the order and replacement of records while leaving the maps shared. Before proposing a “deep copy,” identify which nested data must become independent and how to handle cycles or immutable objects. Blindly copying every reachable object can be expensive and semantically wrong.
- Read a window briefly: a subslice is suitable when the caller keeps the underlying data valid and stable.
- Prevent append from reaching later slots: a full slice expression restricts capacity; existing elements still share storage.
- Replace scalar elements independently: clone or allocate and copy; nested references remain shared.
- Transfer exclusive mutation rights: establish an ownership convention that requires previous users to stop writing.
Why does append inside a function need a returned slice?
A function receives its own slice value. Replacing that local value does not replace the caller's value. This original example exposes both the visible mutation and the unchanged caller length:
func add(s []int) []int {
s[0] = 8
return append(s, 9)
}
// Inside main:
s := make([]int, 1, 3)
s[0] = 1
t := add(s)
fmt.Println(s, len(s))
fmt.Println(t, len(t))
fmt.Println(s[:2])
The output is [8] 1, [8 9] 2, and [8 9]. Returning the slice communicates the new length to the caller that accepts it. The earlier element write was visible even without that assignment because both views reached the same array.
Prefer s = add(s) when the API returns the updated sequence. A pointer to a slice can also let a function replace the caller's slice value, but it is a different API contract, not a requirement for mutating elements. Explain why the extra indirection is needed before introducing it.
Our review question is simple: could callers incorrectly discard the returned value while assuming their sequence grew? If yes, improve the API documentation and add a test that checks length as well as contents. Testing only the first modified element misses the actual contract failure.
Why can a tiny slice retain a large allocation?
Official fact: the Go project's slice internals article explains that a subslice can retain the underlying array even when only a small region is needed. A capacity limit changes the view; it does not extract those bytes into a smaller allocation.
Imagine a parser that reads a large file and caches a short identifier as a subslice. The identifier's logical size can be tiny while its retained storage is much larger. If the identifier must outlive parsing, copying the needed bytes into independent storage is a useful repair to evaluate.
That is a reachability argument, not a promised immediate drop in resident memory. Other references can retain the old allocation, garbage collection happens later, and the allocator need not promptly return memory to the operating system. Use a heap profile to identify retained objects rather than inferring a leak from one process-memory number.
A related issue appears when removing pointer elements. Official project guidance: Robust generic functions on slices discusses clearing obsolete tail elements so they do not retain objects unnecessarily. In custom deletion code, shortening the visible slice alone may leave references in its backing storage. Clear the appropriate vacated region only when doing so respects other views' ownership contracts.
How should you test the concurrency and growth boundaries?
Do not make correctness depend on a slogan such as “capacity always doubles.” The language contract settles whether existing storage is sufficient; it does not promise your preferred growth sequence. In our tests, assertions compare content and aliasing behavior, not a guessed capacity after allocation.
Use two construction paths for a function under review: one with spare capacity, another with length equal to capacity. This forces the reuse and allocation branches. Add a sibling view over the region that an append could overwrite. A test that always starts with a full slice can accidentally hide the reuse bug.
Separate slice variables do not automatically make concurrent writes safe. Two goroutines can race through different views of the same element, or append into overlapping spare capacity. A channel handoff needs an ownership rule: after sending a slice, does the sender stop touching that storage, or does it send an independent copy?
Official tool guidance: the Go race detector detects races on executed paths. Run realistic tests with -race, but do not interpret one clean run as proof that all schedules are safe. Our local fixture verifies the sequential examples and additional aliasing boundaries; it is not a concurrent-system certification.
Practice explaining the storage before changing the code
These PracHub exercises cover Go design or related collection reasoning. The general data-structure exercises are transfer practice: use Go slices to explore the stated boundary, rather than assuming their original prompt specifically tests Go append behavior.
| PracHub practice question | Slice-focused follow-up |
|---|---|
| Design a Go music player with playlists | Decide whether returned playlists are borrowed views or snapshots |
| Explain Go concurrency and database choices | Define who may mutate a batch after a goroutine handoff |
| Array vs. Linked List: Differences and When to Use Each | Separate array storage from a resizable slice view |
| Implement a custom list with iterator and map | State what mutations invalidate an iterator's assumptions |
| Design a deck of cards with shuffle/draw | Prevent a returned hand from sharing unintended mutation rights |
Start with Design a Go music player with playlists. Write the ownership contract for returning, sorting, and extending a playlist, then test it with both spare and exhausted capacity.
Comments (0)