Understanding and Avoiding Pitfalls of Closures in Go
The article explains Go closures, shows how capturing loop variables by reference can cause bugs like repeated values, demonstrates correct patterns such as copying variables inside loops or passing them as parameters, and offers guidelines to avoid common pitfalls with defer and concurrency.
The article introduces the concept of closures in Go, explains why they can cause subtle bugs, and provides practical guidelines to use them correctly.
Background
A closure is a function together with the lexical environment that captures variables from its surrounding scope. Misusing closures—especially inside loops—can lead to unexpected results, as illustrated by a real incident where a closure bug caused a production failure.
Problem illustration
The following Go code stores anonymous functions (closures) in a slice and later executes them:
func main() {
// Save function closures
var s []func()
for _, v := range []string{"a", "b", "c", "d", "e"} {
s = append(s, func() {
fmt.Printf("value: %v\n", v)
})
}
for _, f := range s {
f()
}
}One might expect the output to be a b c d e , but the actual result is:
value: e
value: e
value: e
value: e
value: eThe reason is that the loop variable v is captured by reference; all closures share the same variable, which ends up holding the last value ( e ) after the loop finishes.
Correct usage
Declare a new variable inside the loop so each closure captures its own copy:
func main() {
var s []func()
for _, v := range []string{"a", "b", "c", "d", "e"} {
v := v // create a new variable for this iteration
s = append(s, func() {
fmt.Printf("value: %v\n", v)
})
}
for _, f := range s {
f()
}
}Now the output matches the expectation:
value: a
value: b
value: c
value: d
value: eAdditional examples
Anonymous functions can also be used with defer . The following demonstrates the difference between a closure (reference capture) and a regular value capture:
func main() {
i := 0
// Closure: i is captured by reference
defer func() {
fmt.Println("defer closure i:", i)
}()
// Non‑closure: i is passed by value
defer fmt.Println("defer i:", i)
i = 100
return
}
// Output
// defer i: 0
// defer closure i: 100Common pitfalls and how to avoid them
Using a closure inside a for range loop without copying the loop variable.
// Wrong – prints 3 3 3
s := []int{1, 2, 3}
for _, v := range s {
go func() { fmt.Println(v) }()
}
select {}
// Correct – prints 1 2 3
s := []int{1, 2, 3}
for _, v := range s {
go func(v int) { fmt.Println(v) }(v)
}
select {}Using a closure in a defer statement where the captured variable changes after the defer is set.
// Wrong – y becomes 102 before the deferred function runs
package main
import "fmt"
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y) // y is 102
}(x)
x += 100
y += 100
}
// Fixed – avoid defer with a closure or copy needed values
package main
import "fmt"
func main() {
x, y := 1, 2
func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y) // y is 2
}(x)
x += 100
y += 100
fmt.Println(x, y)
}Summary
Closures are powerful for creating private state, callbacks, and higher‑order functions, but they must be used with care. Pay attention to variable scope, avoid capturing loop variables directly, and be mindful of how defer interacts with closures to prevent unexpected behavior and potential memory or performance issues.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.