How to Handle Loop Variable Capture Issues in Go 1.22+: JetBrains Guidelines
Go 1.22 automatically creates distinct copies of loop variables for each iteration, eliminating the need for defensive copy patterns like v := v when using closures or goroutines.
The JetBrains/go-modern-guidelines repository provides authoritative recommendations for writing modern Go code, including specific guidance on handling loop variable capture issues in Go 1.22+. Prior to Go 1.22, loop variables were reused across iterations, causing subtle bugs when goroutines or closures captured references. The language specification changed in Go 1.22 to address this at the compiler level, and the JetBrains tool enforces this improvement through the loopvar_capture guideline.
Understanding Go 1.22 Loop Variable Semantics
Starting with Go 1.22, each iteration of a for loop receives its own distinct copies of the loop variables. This semantic change means the classic "defensive copy" pattern is no longer necessary to prevent closures from capturing the wrong value.
According to the JetBrains/go-modern-guidelines source code, the guideline is defined in [internal/guidelines/guidelines.json](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json#L653-L660) (lines 653-660) and validated by tests in [internal/guidelines/guidelines_test.go](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines_test.go#L17-L18) (lines 17-18). The rule is classified as High-impact under the Loops category, indicating it significantly affects code correctness and readability.
Removing Redundant Defensive Copies
In versions prior to Go 1.22, developers wrote defensive copies to ensure closures captured the correct iteration value:
// Before (redundant in Go 1.22+)
for _, item := range items {
item := item // unnecessary defensive copy
go func() {
process(item) // captures the copy
}()
}
With Go 1.22+, you can safely remove the explicit copy because the compiler provides a fresh item variable for every iteration:
// After (idiomatic Go 1.22+)
for _, item := range items {
go func() {
process(item) // captures the per-iteration variable automatically
}()
}
The internal/guidelines/guidelines.go file generates the human-readable explanations shown by the ListText and ExplainText commands, simplifying the migration from legacy patterns.
When to Keep Explicit Variable Copies
While the v := v idiom is generally obsolete, you must preserve the original behavior in one specific scenario: when intentionally needing a pointer to the original slice element.
If your code takes the address of a slice element using &slice[i], omitting the copy would give you the address of the iteration variable (which now changes per iteration) rather than the underlying array element. Keep the address-of operation directed at the original slice:
var selected []*Item
for i := range items {
if items[i].Enabled {
// Take address of the element in the underlying slice
selected = append(selected, &items[i])
}
}
In this pattern, do not introduce item := item because you need the address of the original slice element, not a copy of the iteration variable. The guideline specifically targets closures, goroutines, defer calls, and append(&v) scenarios where the copy was historically used to avoid capture bugs.
Summary
- Go 1.22 eliminates manual loop-variable copies for closures, goroutines, and deferred functions by providing fresh variables per iteration.
- Remove the
v := vidiom flagged by theloopvar_captureguideline unless you specifically need a pointer to the original slice element. - The
JetBrains/go-modern-guidelinestool categorizes this as a High-impact recommendation stored ininternal/guidelines/guidelines.json. - Clean up legacy defensive copies to produce more idiomatic, readable Go code.
Frequently Asked Questions
What changed in Go 1.22 regarding loop variable capture?
Go 1.22 changed the language specification so that each iteration of a for loop creates new instances of the iteration variables (the index and value variables in for i, v := range). Previously, these variables were allocated once and reused, causing closures to capture the final value rather than the iteration-specific value. Now, each closure automatically captures the correct per-iteration value without manual copying.
Why does the JetBrains tool flag item := item as unnecessary?
The loopvar_capture guideline in the JetBrains/go-modern-guidelines repository identifies the v := v pattern as redundant because Go 1.22's compiler already provides distinct variable instances for each loop iteration. The tool references the guideline definition in internal/guidelines/guidelines.json and generates warnings through the analysis engine to help developers modernize their codebases.
When should I still use explicit loop variable copies in Go 1.22+?
Keep the explicit copy only when you need to take the address of the original slice element using &slice[i]. If you introduce item := item in this scenario, you would capture the address of the copy (a loop variable) rather than the underlying array element, which could lead to incorrect data or memory aliasing issues in your program.
Where can I find the loopvar_capture guideline in the repository?
The guideline is defined in [internal/guidelines/guidelines.json](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines.json#L653-L660) at lines 653-660, with validation tests located in [internal/guidelines/guidelines_test.go](https://github.com/JetBrains/go-modern-guidelines/blob/main/internal/guidelines/guidelines_test.go#L17-L18) at lines 17-18. The human-readable text generation is handled by internal/guidelines/guidelines.go, which powers the ListText and ExplainText CLI commands.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →