← Debugging Guides

阅读中文版本

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.

advanceddebugginggoconcurrencyloop-variable

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.