Go Range Loop Variable Address Same — Shared Pointer Bug
Fix goroutines or closures capturing the same range variable address by copying the loop value per iteration or using Go 1.22 per-iteration semantics.
Symptoms
- Every goroutine or closure prints the same last element
- A slice of pointers collected in a loop all point to the same value
- Results are correct with sequential code but wrong under go vet or -race
Possible causes
- &v is taken where v is the reused range variable (pre Go 1.22)
- A closure captures the loop variable by reference and runs later
- A pointer slice is appended with the address of the loop variable instead of a copy
Diagnosis steps
Detect the pattern with vet
go vet ./...Interpretation
go vet flags loop-variable capture (loopclosure); newer toolchains also detect the address case.
Reproduce under the race detector
go test -race ./...Interpretation
A shared address across iterations shows as goroutines observing one mutated value.
Root cause
Before Go 1.22 there was a single range variable reused across iterations. Taking its address or capturing it in a closure therefore shares one storage location, so all references observe the last assigned value.
Fix
Copy per iteration (v := v) before capturing, pass the value as a function argument, or upgrade to Go 1.22+ where each iteration has its own variable.
Risk note
Relying on Go 1.22 semantics requires updating go.mod go directive; an older directive keeps the old behavior at build time.
Verify the fix
Rerun under -race and confirm each goroutine sees its own element. Keep go vet in CI so the pattern cannot regress.
Symptoms
This debugging guide addresses “Go Range Loop Variable Address Same — Shared Pointer Bug”. Start from the symptoms, follow the ordered diagnostics, and confirm each hypothesis before moving on; the fix and verification steps then close the loop.
Observed symptoms include:
- Every goroutine or closure prints the same last element
- A slice of pointers collected in a loop all point to the same value
- Results are correct with sequential code but wrong under go vet or -race
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Detect the pattern with vet
go vet ./...
go vet flags loop-variable capture (loopclosure); newer toolchains also detect the address case.
Step 2: Reproduce under the race detector
go test -race ./...
A shared address across iterations shows as goroutines observing one mutated value.
Likely contributors, in order of frequency:
- &v is taken where v is the reused range variable (pre Go 1.22)
- A closure captures the loop variable by reference and runs later
- A pointer slice is appended with the address of the loop variable instead of a copy
Root Cause
Before Go 1.22 there was a single range variable reused across iterations. Taking its address or capturing it in a closure therefore shares one storage location, so all references observe the last assigned value.
The Fix
Copy per iteration (v := v) before capturing, pass the value as a function argument, or upgrade to Go 1.22+ where each iteration has its own variable.
Watch out: Relying on Go 1.22 semantics requires updating go.mod go directive; an older directive keeps the old behavior at build time.
// ❌ shared address (pre Go 1.22)
var ptrs []*Item
for _, it := range items {
ptrs = append(ptrs, &it) // all point to the same reused variable
}
// ✅ copy per iteration (works on any Go version)
for _, it := range items {
it := it
ptrs = append(ptrs, &it)
}
// ✅ or index into the slice, which is stable
for i := range items {
ptrs = append(ptrs, &items[i])
}
Verify and Prevent
Rerun under -race and confirm each goroutine sees its own element. Keep go vet in CI so the pattern cannot regress.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.