ProgrammingGo Developer

How does the for-init postfix declaration of a loop work in Go, and why can the scope of the loop variable lead to elusive bugs when used in goroutines and closures?

Pass interviews with Hintsage AI assistant

Answer.

In Go, the for loop construct can include an initialization block (init), condition check, and a postfix expression. Historically, this mechanism was created for the convenience of writing code and the familiarity of C-like languages. However, in Go, the scope of the loop variable (i) has specific characteristics that significantly affect its behavior inside nested functions, closures, and goroutines.

Problem — When running goroutines or closures on each iteration of the loop, unexpected behavior often occurs: the variable i is not copied but "captured" by reference, meaning the closure refers to a shared loop variable, which takes the last value after the loop ends. This leads to the same result across all goroutines/closures, even though the logic might have suggested otherwise.

Solution — If you need to pass the value of the variable from each iteration, use explicit copying of the variable (via an additional variable) or pass it as an argument to the closure.

Code example:

for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // Correct! Copied value } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Error: all goroutines will print 3 }

Key features:

  • In the for loop, the loop variable is implicitly declared in the scope of the for block.
  • Capturing the loop variable in a closure/goroutine will lead to "sharing" the variable among all instances of the closure.
  • This is circumvented by copying the variable into a new variable in each iteration.

Tricky Questions.

Does the scope of the for variable change when using break or continue?

No. The scope of the variable declared in for is always limited to the block of that loop. Break or continue only interrupts the current iteration but does not "propagate" the variable outside.

Can a variable declared in the init part of for be captured within a method outside the loop?

No. The variable is only visible within the for loop itself and all nested blocks but not outside it after the loop exits.

What happens if the variable is captured in a defer expression inside for?

The same situation: the defer function will "see" not the value at the time of creation but the current value of the variable at the time of executing defer (usually the last value of the loop).

for i := 0; i < 3; i++ { defer fmt.Println(i) // all defer will print 3 }

Common mistakes and anti-patterns

  • Capturing the loop variable without copying it into a new variable.
  • Passing the loop variable to an anonymous function without explicitly passing it (late binding effect).
  • Using defer inside a loop without considering the scope of variables.

Real-life example

Negative case

In a Go web server, a developer launched several goroutines to handle different ports, using the port index as the loop variable and capturing it directly in a lambda expression. All goroutines accessed one port — the last one in the array.

Pros:

  • Simple, "explicit" loop implementation.

Cons:

  • Incorrect logic.
  • Hard-to-debug bug.

Positive case

The team established a rule — always copy the loop variable's value into a new variable that the closure/goroutine will capture.

Pros:

  • No unexpected side effects.
  • Transparency of the code.

Cons:

  • Loss of "micro-optimizations" (yet another variable on the stack, but insignificant).