Last modified: Jan 23, 2026 By Alexander Williams
Go Variable Scope in for Loops Explained
Understanding variable scope is key to writing good Go code. This is especially true inside for loops. A small mistake can cause big bugs.
This guide explains variable scope in Go loops. You will learn how to avoid common errors. We will also cover best practices for clean code.
What is Variable Scope?
Scope defines where a variable can be used. In Go, a variable declared inside a block is local to that block. It cannot be accessed from outside.
Blocks are created by curly braces {}. This includes function bodies, loop bodies, and if statements.
Knowing scope helps you manage variable lifetimes. It prevents naming conflicts and unexpected behavior. For a deeper dive into declaration styles, see Go Variable Declaration: var vs := Explained.
Basic Scope in a for Loop
Let's start with a simple loop. Variables declared inside the loop body have a limited scope.
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
// 'value' is scoped to this loop iteration
value := i * 2
fmt.Println("Inside loop:", value)
}
// fmt.Println(value) // This would cause a compile error: undefined: value
}
Inside loop: 0
Inside loop: 2
Inside loop: 4
The variable value is created anew in each iteration. It dies at the end of each loop body. You cannot use it after the loop.
The loop variable i is also scoped to the for statement block. It is not accessible after the loop ends.
The Classic Closure Pitfall
A common trap involves closures and goroutines inside loops. The issue relates to how variables are captured.
Look at this problematic example.
package main
import (
"fmt"
"time"
)
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
// WARNING: This has a scoping bug!
go func() {
fmt.Println(i) // Captures the variable 'i'
}()
}
time.Sleep(time.Second) // Wait for goroutines to finish
}
3
3
3
All goroutines print 3. Why? The anonymous function captures the variable i itself, not its value at that moment.
By the time the goroutines run, the loop has finished. The final value of i is 3. All goroutines share this single variable.
This is a frequent source of confusion. It often leads to data races and incorrect program output.
How to Fix the Closure Problem
You must create a new variable scoped to each iteration. This breaks the shared reference. There are two main solutions.
Solution 1: Pass the Value as a Function Argument
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
// Pass 'i' as an argument, creating a new scope
go func(val int) {
fmt.Println(val) // Uses the local argument 'val'
}(i) // 'i' is evaluated here
}
time.Sleep(time.Second)
}
2
0
1
The value of i is copied into the function argument val. Each goroutine gets its own copy. The order of output may vary due to goroutine scheduling.
Solution 2: Create a New Local Variable
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
// Create a new variable scoped to this iteration
val := i // 'val' is a new variable each time
go func() {
fmt.Println(val) // Captures the iteration-scoped 'val'
}()
}
time.Sleep(time.Second)
}
Declaring val := i inside the loop body creates a fresh variable. Each closure captures a different val. This solves the problem neatly.
For more on managing short-lived data, review Go Best Practices for Temporary Variables.
Scope in Range Loops
The same rules apply to for...range loops. However, the loop variables are reused in each iteration.
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
var pointers []*int
for _, v := range nums {
// BAD: Taking the address of the loop variable 'v'
pointers = append(pointers, &v)
}
for _, p := range pointers {
fmt.Print(*p, " ")
}
}
30 30 30
This prints "30 30 30". The slice stores pointers to the same variable v. Its final value is 30.
The fix is the same. Create a local copy inside the loop.
for _, v := range nums {
localCopy := v // Correct: New variable each iteration
pointers = append(pointers, &localCopy)
}
10 20 30
Now each pointer points to a unique integer. The output is correct.
Best Practices and Key Takeaways
Follow these rules to avoid scope-related bugs in your loops.
1. Be Explicit with Goroutines and Closures. Always pass loop variables as arguments to closures. Or create a new local variable. Never capture the loop variable directly.
2. Understand Variable Reuse. In for and range loops, the iteration variable is reused. Its address does not change. Plan your logic accordingly.
3. Keep Loop Bodies Focused. Declare variables as close to their use as possible. This limits their scope and reduces side effects. It makes your code easier to read and debug.
If you encounter strange values from concurrent operations, check your scope first. For other common issues, Fix Common Go Variable Errors can help.
Conclusion
Variable scope in Go for loops is straightforward but has sharp edges. The loop variable is reused across iterations. Closures capture variables by reference, not value.
This leads to the classic bug where goroutines all see the final loop value. The fix is simple: create a new scoped variable or pass the value as a function argument.
Mastering this concept is crucial for writing correct concurrent code in Go. Always think about where your variables live and what captures them. Your future self will thank you for the clean, predictable code.