In Go, why does ranging over a slice of structs with for _, r := range rows { r.count++ } leave rows unchanged?
answer
- the body gets its own variable
- assignment in Go copies
- the element is copied in, not aliased
- index into the slice to write
- the copy dies with the iteration
basics
~20 srange assigns a copy of each element into r, so r.count++ increments a copy that is thrown away at the end of the iteration. To modify the slice, index into it: for i := range rows { rows[i].count++ }.
solid answer
~50 sThe second range variable is an ordinary variable that gets a **copy** of the element assigned to it on every iteration, exactly as `r := rows[i]` would. Mutating a field of that copy changes the copy, and the copy is discarded when the iteration ends, so `rows` is untouched. The fix is to go through the index: `for i := range rows { rows[i].count++ }`, which writes into the backing array. The trap disappears if the element type is a pointer - with `[]*row`, the copy is a copy of the pointer, so `r.count++` updates the shared struct behind it, though reassigning `r` itself still changes nothing. The same copy has a cost: ranging over a slice of large structs copies the whole struct each iteration, which is a reason to range by index in a hot loop.
code
go · 14 linestype row struct {
name string
count int
}
rows := []row{{"a", 1}, {"b", 2}}
for _, r := range rows {
r.count++ // updates the copy; rows is untouched
}
for i := range rows {
rows[i].count++ // updates the element in place
}go deeper
Recall the rule as a sentence: the second range variable is a copy. Practise the index rewrite until it is automatic, because this is the single most common Go loop bug a junior ships.
Explain why it is a copy - range performs an assignment, and assignment copies in Go - and show the pointer-element case where mutation is visible. Be ready to say what happens with a pointer-receiver method called on the copy.
Bring the performance half: name the size at which the per-iteration copy matters, say you would confirm it with a benchmark rather than guessing, and describe how you would catch the mutating-the-copy pattern in code review.
Frame it as an API question - whether a collection holds values or pointers decides who can mutate what, how much copying the callers pay, and whether nil elements are possible. That choice is worth making deliberately at the type, not loop by loop.
## Assignment copies, and range is an assignment Go has value semantics: assigning a struct copies it, passing a struct to a function copies it, and storing a struct into a slice copies it. The `range` clause is no exception. On each iteration the runtime performs the equivalent of ```go i, r := index, rows[index] ``` and `r` is a fresh, independent value. `r.count++` reads and writes a field of that independent value. When the iteration ends the value is discarded, and `rows` never learned anything happened. This is one of the most common surprises for people coming from Python, Java, or JavaScript, where the loop variable binds a reference to the object and mutating through it is visible to the container. In Go, whether a mutation is visible is decided by the element **type**, not by the loop. ### The three fixes, and which is right **Index into the slice.** The direct answer: ```go for i := range rows { rows[i].count++ } ``` `rows[i]` is an addressable element of the backing array, so the write lands in the slice. This is the idiomatic in-place update and it also avoids copying the element at all. **Write the copy back.** Legal but noisy: ```go for i, r := range rows { r.count++ rows[i] = r } ``` It works, and occasionally reads well when the body builds a substantially new value, but for a single field bump it is two copies where zero were needed. **Hold pointers.** If the slice is `[]*row`, then `r` is a copy of a pointer and `r.count++` dereferences to the one shared struct, so the update is visible. This is a real design choice with real consequences elsewhere - pointer elements mean extra indirection, more heap traffic, and the possibility of nil elements - so choose it because the data wants sharing, not to make a loop compile. ### The method-call variant of the same trap ```go for _, r := range rows { r.Increment() // pointer receiver on an addressable copy } ``` If `Increment` has a pointer receiver, this compiles: `r` is a local variable and therefore addressable, so the compiler takes `&r` for you. It mutates the copy. The loop looks like it is calling a mutating method on each element and it is calling it on each throwaway duplicate. This version is harder to spot in review than the plain field write, because nothing on the line looks like an assignment. ### The cost side of the same fact Copying is not just a correctness question. Ranging over `[]bigStruct` where `bigStruct` is a few hundred bytes copies those bytes on every iteration. In a batch job that scans a few million CSV rows into a slice of wide structs, that is real time and real memory bandwidth, and it shows up in a profile as time in the loop itself with no obvious callee. Ranging by index and touching `rows[i]` directly removes the copy entirely: ```go for i := range rows { total += rows[i].amount } ``` That said, do not rewrite loops on instinct. For small elements - ints, strings, two-word structs - the copy is free or nearly so, and `for _, v := range` is clearer. Measure with a benchmark before trading readability for the index form. ### What a reviewer looks for On a pull request, the pattern worth flagging is a range body that writes to the value variable or calls a mutating method on it, with no write back into the collection. Nine times out of ten the author expected the mutation to stick. The second pattern is a range over a slice of wide structs in a loop that runs millions of times, where the index form is both faster and no less readable. The third is `for _, r := range rows` where `r` is then aliased into a longer-lived structure - the copy is fine there, but the author often believes they stored the element rather than a snapshot of it. ### The array footnote Ranging over an **array** value copies the array itself once, before iteration begins, so even `for i := range arr { arr[i] = 0 }` reads its length from the original while writing to the original - but a copy taken through an intermediate variable is a genuinely separate array. When you find yourself reasoning about this, converting to a slice with `arr[:]` and ranging over that removes the ambiguity.
- How would you rewrite the loop so it does modify the slice?Range by index and write through the index: `for i := range rows { rows[i].count++ }`. `rows[i]` names the element in the backing array, so the increment lands in the slice, and no copy of the element is made at all. Writing the modified copy back with `rows[i] = r` also works but copies the struct twice for no benefit.
- Does the same trap apply when the slice holds pointers, as in []*row?No, for field writes. The copy is a copy of the pointer, so `r.count++` dereferences the same struct every reader sees and the change is visible. Reassigning the variable itself - `r = &other` - still changes nothing, because only the local pointer variable is replaced. The visibility comes from the element type being a pointer, not from anything range does differently.
- When does the per-iteration copy actually cost you anything?When the element type is large and the loop is hot. A slice of 300-byte structs scanned a few million times copies close to a gigabyte of data for nothing, and it shows in a profile as time inside the loop with no callee to blame. Ranging by index and reading `rows[i].field` removes the copy. For small elements the copy is negligible and the value form is clearer.
range hands the body a photocopy of each row, not the row itself. Writing on the photocopy is legal, and the filing cabinet is unchanged when you throw it away.
saying these in an interview costs you the question
- Says the range value variable aliases the element
- Thinks a pointer-receiver method on the copy updates the slice
- Claims the compiler optimised the write away
- Believes only pointer slices can be modified in place
- Assumes ranging a slice of large structs copies nothing