Loop Var Capture

ID

go.loop_var_capture

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Go

Tags

concurrency, reliability

Description

Reports a for/range loop variable captured by reference inside a closure that is launched with go or scheduled with defer in the same loop.

Rationale

Before Go 1.22 the loop variable was a single binding reused across iterations, so every closure captures the same variable and observes its final value instead of the value at launch time — a classic concurrency bug that fails intermittently. Passing the value as an argument, or shadowing it into a fresh per-iteration variable, makes each closure see the intended value. Go 1.22 changed the semantics so each iteration gets a fresh variable, but the pattern remains a defect for modules targeting earlier language versions and is a portability/readability smell either way.

// Bad
for _, v := range xs {
    go func() { use(v) }()      // FLAW — all goroutines share the one v
}

// Good
for _, v := range xs {
    go func(s T) { use(s) }(v)  // OK — value passed as an argument
}
for _, v := range xs {
    v := v                      // OK — fresh per-iteration variable
    go func() { use(v) }()
}

Remediation

Pass the loop variable to the closure as an argument, or shadow it with v := v at the top of the loop body so each iteration captures its own copy.