If you append to a Go slice while ranging over it, does the loop visit the newly appended elements?
answer
- the range expression is read once
- the length is fixed before iteration
- growing it mid-loop changes nothing visible
- no modification exception exists here
- a three-clause loop re-reads len each pass
basics
~20 sNo. range evaluates the slice expression once and fixes the iteration count from its length before the first pass, so the loop runs exactly the original len(s) times. Appended elements are in the slice afterwards but are never visited.
solid answer
~50 sThe range expression is evaluated **once**, before the first iteration, and for a slice the length is captured at that point. So `for _, v := range s { s = append(s, x) }` runs exactly `len(s)` times as measured at entry - the slice really does grow, and the loop really does ignore the growth. There is no concurrent-modification panic; Go simply never promised to re-read the length. A second subtlety: if an `append` reallocates, `s` now points at a new backing array while the loop keeps reading the one it captured, so later writes through `s[i]` are invisible to the loop as well. When you genuinely need a work list that grows as you process it, use a three-clause loop - `for i := 0; i < len(queue); i++` re-evaluates `len` on every iteration - and make sure something bounds the growth so it terminates.
code
go · 10 liness := []int{1, 2, 3}
for _, v := range s {
if v == 1 {
s = append(s, 99)
}
}
// the body ran exactly 3 times; 99 was never visited
fmt.Println(len(s)) // prints: 4go deeper
Take away the single rule: the number of iterations is decided before the loop starts, so appending inside a range loop does not lengthen it. Do not expect an error - the code runs quietly.
Explain the mechanism: the operand is evaluated once and the slice header, including its length, is captured. Be able to contrast that with the map rules, where deletion is defined and insertion is unspecified.
Show the diagnosis and the fix: recognise the growing-work-list requirement, reach for the three-clause form that re-reads len, and immediately supply the termination argument - a seen set and a cap - because that loop's failure mode is a pinned core and climbing memory.
Own it as a review standard: any loop that appends to the collection it walks needs a written reason it terminates, and unbounded queues need a cap so a logic bug degrades into a bounded error rather than an outage.
## The range expression is evaluated once This is the sentence to hold on to: the operand of a `range` clause is evaluated a single time, before the loop starts. For a slice, what gets captured is the slice value - the pointer to the backing array, the length, and the capacity. The loop then iterates from 0 up to that captured length. Nothing re-reads `len(s)`, and nothing notices that `s` the variable has been reassigned. ```go s := []int{1, 2, 3} for _, v := range s { if v == 1 { s = append(s, 99) } } // the body ran exactly three times; 99 was never visited ``` After the loop, `len(s)` is 4. The 99 is there. The loop just never saw it. ### Why no panic Engineers arriving from languages whose iterators fail fast on structural modification expect an exception here. Go's slices carry no modification counter and no iterator object to invalidate; `range` over a slice compiles down to an index loop against a captured header. There is nothing to detect and nothing to throw. The behaviour is fully defined - it just is not the behaviour people assume. ### The reallocation subtlety Suppose the append exceeds capacity. `append` allocates a larger array, copies the elements, and returns a new slice header, which you assign back to `s`. The loop is still walking the **old** array through the header it captured. From that point: - writes the body makes through `s[i]` land in the new array and are invisible to the remaining iterations; - writes the body makes before any reallocation, through the same array, *are* visible to later iterations, because the header points at that array. So "does the loop see my write?" has the same answer as everywhere else in Go slice work: it depends on whether an append moved the backing array. This is exactly the aliasing question that makes slice-mutating helpers hard to reason about, and it is a good reason not to reassign the ranged variable inside the loop at all. ### Maps are a different rule Modifying a map during `range` has its own defined-but-loose behaviour, and it is worth knowing precisely because it is not the slice rule: - an entry **removed** before the loop reaches it is guaranteed **not** to be produced; - an entry **added** during iteration **may or may not** be produced - it is unspecified, and it may differ between runs. Deleting the current key inside the loop is safe and idiomatic. Adding keys while iterating is not something to build on: collect what you want to add into a separate slice and apply it after the loop. (None of this concerns concurrent access from other goroutines, which is a different problem with a different failure.) ### The right shape for a growing work list A batch job that walks a directory of CSV files often has this shape: process a file, discover that it references others, and add those to the pending list. `range` is the wrong tool, because the list you must process is not the list you started with. The three-clause form re-evaluates the condition on every pass: ```go for i := 0; i < len(pending); i++ { next := process(pending[i]) pending = append(pending, next...) } ``` `len(pending)` is read fresh each iteration, so items appended during the loop are picked up. That is the intended behaviour - and it is also the classic way to write a loop that never terminates. If `process` can return something that leads back to a file already handled, the list grows at least as fast as `i` advances and the loop spins forever, allocating as it goes. This is not a hang: the process pins a core and its memory climbs, which is how it shows up in production. The remedy is the one every work-list algorithm uses - a `seen` set keyed by a canonical identity, checked before anything is appended, plus a hard cap on the queue length so that a bug becomes a bounded error rather than an unbounded one. ### Reviewing for this Three things are worth a comment on a pull request. A `range` loop that appends to the collection it is ranging over is almost always a misunderstanding - either the author wanted a growing work list and needs the three-clause form, or the append belongs to a different slice. A three-clause loop over a collection the body also appends to needs a visible reason it terminates. And a loop that reassigns the ranged slice variable at all is worth questioning, because after the first reallocation the loop and the variable disagree about which array they mean. ### The one-line summary to give in an interview "The range expression is evaluated once, so the iteration count is fixed at entry; if you need a collection that grows while you process it, use a three-clause loop that re-reads len, and give it a termination argument."
- What are the rules for adding or deleting map entries during a range over that map?Deleting an entry the loop has not reached yet guarantees it will not be produced, and deleting the current key is safe and idiomatic. Adding an entry during iteration is unspecified - it may or may not be produced, and the outcome can differ between runs - so never build on it. If you must add, collect the additions in a slice and apply them after the loop finishes.
- How do you iterate a work list that grows while you process it?Use the three-clause form, `for i := 0; i < len(queue); i++`, which re-evaluates `len(queue)` on every pass so appended items are processed. Then supply the termination argument: a `seen` set checked before every append so an item cannot be enqueued twice, and usually a hard cap on the queue length so a bug becomes a bounded failure instead of an infinite loop.
- Does the loop see a write like s[2] = 9 made earlier in the same range loop?Usually yes: the captured header points at the same backing array the write lands in, so a later iteration reads the new value. It stops being true once an append inside the loop has reallocated - after that, `s` names a new array while the loop keeps reading the old one, and further writes through `s[i]` are invisible to it.
- Does the same once-only evaluation rule apply to the other range forms?Yes, the operand is evaluated once in every case. For an integer, the bound is fixed at entry, so changing the variable you ranged over inside the body has no effect on the iteration count. For a map there is no length to capture, which is why maps get their own add and delete rules instead.
saying these in an interview costs you the question
- Says the loop will iterate the appended elements
- Expects a concurrent-modification panic when the slice grows
- Claims range re-reads len on every iteration
- Thinks deleting a map key during range is undefined
- Believes range holds a live reference to the slice variable