skip to content

A builder calls reflect.MakeSlice(t, n, n), then reflect.Append(s, v) in a loop, yet the result holds n zero values and none of the appended ones. What are the two bugs?

level: seniorimportance: should knowfreq 34%

answer

  1. two plain ints, nothing distinguishes them
  2. one argument counts elements that already exist
  3. the reflective form copies the builtin's contract
  4. the builtin is protected by the compiler here
  5. the returned header carries the new length

basics

~20 s

reflect.MakeSlice takes length second and capacity third, so MakeSlice(t, n, n) starts with n zero elements; it should be MakeSlice(t, 0, n). And reflect.Append returns a new Value, so its result must be assigned back.

solid answer

~40 s

Two independent mistakes stack up. First, `reflect.MakeSlice(typ, len, cap)` takes **length** second and **capacity** third, so `MakeSlice(t, n, n)` produces a slice that already contains `n` zero-valued elements — the caller wanted `MakeSlice(t, 0, n)`, pre-sized but empty. Second, `reflect.Append` has the same contract as the `append` builtin: it returns a **new** `reflect.Value` and does not modify its argument, so `reflect.Append(s, v)` as a bare statement throws the result away. The fix is `s = reflect.Append(s, v)`. The reason this survives compilation is that the builtin `append` is protected — discarding its result is a compile error — while `reflect.Append` is an ordinary function whose result may legally be ignored, and `go vet`'s unusedresult check does not cover it by default. A test that asserts `s.Len()` after the loop catches both at once.

code

go · 6 lines
go
et := reflect.TypeOf(Config{})
s := reflect.MakeSlice(reflect.SliceOf(et), 0, 4) // len 0, cap 4
for i := 0; i < 3; i++ {
	s = reflect.Append(s, reflect.New(et).Elem()) // must reassign
}
fmt.Println(s.Len(), s.Cap()) // prints: 3 4

go deeper

for a junior

Learn the two signatures literally: MakeSlice takes type, length, capacity in that order, and Append returns a new Value that you must assign back, exactly like the append builtin does.

for a middle

Explain why reassignment is required even when capacity is sufficient: the argument is a copy of the slice header, so the caller's length never changes no matter which path the append took.

for a senior

Reason from the evidence to the diagnosis — n elements, all zero, is a different fingerprint from length zero or length 2n — and say how you would instrument it and what regression test you would leave behind.

for a principal

Take the API-shape lesson: a helper that appends to a reflect.Value and returns nothing is a trap you have handed every caller, and choosing single-owner mutation over threaded values is the fix that scales past one bug.

## The two bugs, separately ### Bug one: length where capacity was meant `func MakeSlice(typ Type, len, cap int) Value` mirrors `make([]T, len, cap)`. The second argument is the number of elements that **exist** in the slice; the third is how many fit before it must grow. `reflect.MakeSlice(t, n, n)` therefore returns a slice whose `Len()` is already `n`, every element the zero value of the element type. What the author wanted was `reflect.MakeSlice(t, 0, n)`: empty, but with room for `n` appends without a reallocation. The mistake is easy to make because both arguments are plain `int` — nothing in the type system distinguishes them, and swapping them or duplicating one compiles cleanly. `MakeSlice` does panic if `len > cap` or if either is negative, which catches the reversed case `MakeSlice(t, n, 0)`, but `MakeSlice(t, n, n)` is a perfectly legal slice; it is just not the one the author meant. ### Bug two: the discarded result `func Append(s Value, x ...Value) Value` has exactly the contract of the `append` builtin. It may write into the existing backing array if capacity allows, or allocate a bigger one and copy — and either way, the slice **header** it returns (pointer, length, capacity) is what carries the new length. The argument `s` is passed by value, so the caller's header is untouched no matter which path was taken. Discarding the return value therefore discards every append, regardless of capacity. `reflect.AppendSlice(s, t Value) Value` behaves the same way for appending one slice to another. ## Why the compiler catches one form and not the other In ordinary Go, writing `append(s, x)` as a statement and ignoring the result is a **compile error**: `append` is a builtin whose value must be used. That protection is the reason the equivalent bug is rare in hand-written slice code. `reflect.Append` is an ordinary function. Ignoring an ordinary function's result is legal Go — that is how `fmt.Println`'s two return values are ignored a thousand times a day. `go vet` has an `unusedresult` check, but it applies to a fixed list of standard-library functions whose results are always meaningless to drop, and `reflect.Append` is not in that default list. So the safety net that exists for the builtin simply is not there for the reflective form, and the bug compiles, vets and ships. ## Reading the symptom The combined symptom is diagnostic in itself. Length is exactly `n` — the pre-sized length — and every element is the zero value of the element type: empty strings, zeroed ints, nil pointers. If only the second bug were present, the slice would come back with length 0 and no elements at all. If only the first, the slice would have `n` zeros **followed by** the appended values, length `2n`. Seeing `n` zeros and nothing else tells you both are in play, and it is worth saying that out loud in an interview because it shows you reasoned from the evidence rather than pattern-matched. ## How you would actually find it The cheapest instrument is a print of `Len()` and `Cap()` at each step — `fmt.Println(s.Len(), s.Cap())` after `MakeSlice` and again after the loop. `MakeSlice(t, 0, 4)` followed by one append reports `1 4`: length grew, capacity did not, because the append fitted. The buggy version reports `n n` before the loop and `n n` after, and a length that never moves across a loop that is supposed to add elements is the whole story. The durable fix is a test rather than a print. A fixture library's public entry point should have a table test that builds a slice of `k` elements and asserts both `Len() == k` and that element 0 is not the zero value. That assertion fails on either bug independently, which is what you want from a regression test. ## The library-author angle The deeper lesson for someone designing the builder's API is that a `reflect.Value` threaded through helper functions invites exactly this class of bug, because every helper that appends must return the new Value and every caller must reassign it. Two shapes avoid it: - keep the growing slice in **one** place — a small builder struct with a `slice reflect.Value` field and an `add` method that does `b.slice = reflect.Append(b.slice, v)` — so there is a single assignment site to get right rather than one per call site - or size the slice correctly up front with `MakeSlice(t, k, k)` when `k` is known, and fill by `Index(i).Set(v)`, which mutates in place and has no return value to lose Both are defensible; what is not defensible is a helper that takes a slice `reflect.Value`, appends to it, and returns nothing.

  • If only the discarded result were wrong, what would the slice look like?
    It would come back with whatever `MakeSlice` created and nothing more. With `MakeSlice(t, 0, n)` that means length 0 and an empty slice; with `MakeSlice(t, n, n)` it means the `n` zero values. The distinguishing evidence for the pair of bugs is a length exactly equal to `n` with every element still zero — the appended values would otherwise have to appear after the zeros, at length `2n`.
  • Why is discarding reflect.Append legal when discarding the builtin append is not?
    `append` is a builtin, and the language specification requires its result to be used, so the compiler rejects `append(s, x)` as a statement. `reflect.Append` is an ordinary function call, and ignoring an ordinary call's results is always legal Go. `go vet`'s unusedresult check covers a fixed list of standard-library functions and does not include it by default, so no tool in the normal pipeline objects.
  • How would you shape the builder's API so this cannot recur?
    Give the growing slice a single owner. A small builder struct holding `slice reflect.Value` with an `add` method that performs `b.slice = reflect.Append(b.slice, v)` leaves exactly one assignment site instead of one per call. The alternative, when the count is known up front, is `MakeSlice(t, k, k)` and `Index(i).Set(v)`, which mutates in place and returns nothing to drop.
  • What does the length and capacity look like after one append to reflect.MakeSlice(t, 0, 4)?
    Length 1, capacity 4. The append fits inside the existing backing array, so no reallocation happens and capacity is unchanged. That is the point of pre-sizing with capacity: a known number of appends costs one allocation rather than a growth sequence. Printing `Len()` and `Cap()` around the loop is the fastest way to confirm the builder is doing what you intended.

saying these in an interview costs you the question

  • Thinks reflect.Append mutates the slice Value in place
  • Reads MakeSlice's second argument as capacity
  • Expects the compiler or go vet to catch the dropped result
  • Claims the result only matters when the slice reallocates
  • Blames the element type's zero value rather than the length