15. Common Pitfalls
Recognize and avoid subtle bugs and performance traps common in production Go.
Question: What is the "loop variable capture" pitfall, and how has it changed in Go 1.22?
Answer: In Go versions before 1.22, when using a goroutine or closure inside a for
loop, the closure captures a reference to the same loop variable. By the time the goroutines run, the loop is often finished, and all goroutines see the final value of the variable.
Explanation: The classic fix was to create a new copy of the variable for each iteration (v := v
). As of Go 1.22, this is no longer necessary. The for
loop semantics were changed so that each iteration gets its own unique variable, fixing the pitfall. It's important to know both the old problem and the new solution.
// Pre-Go 1.22: all goroutines will likely print "c"
for _, v := range []string{"a", "b", "c"} {
go func() { fmt.Println(v) }()
}
// Pre-Go 1.22 fix (and still safe):
for _, v := range []string{"a", "b", "c"} {
v := v // Shadow variable
go func() { fmt.Println(v) }()
}
Question: Why can an
error
variable be non-nil even if the pointer it holds isnil
?
Answer: An interface value in Go is a pair of (type, value)
. An error
is an interface. If you return a typed pointer (e.g., *MyError
) that has a nil
value, the interface's type component is still set (*MyError
), so the interface itself is not nil
.
Explanation: This is a common gotcha. A function that returns an error should return a plain nil
value, not a typed nil
, to indicate success.
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func Fails() error {
var err *MyError = nil
return err // Returns a non-nil error interface!
}
// In Fails(), err's interface is (*MyError, nil), which is not equal to nil.
Question: Why does
time.After
in a loop leak timers?
Answer: Each time.After(d)
creates a new timer that lingers until it fires and is GC'd.
Explanation: Reuse a single time.Ticker
or create a time.Timer
once and Reset
it inside the loop. Always Stop()
timers/tickers and drain the channel when stopping.
Question: Why can't you write to a
nil
map and what about closed channels?
Answer: Writing to a nil
map panics; you must initialize with make(map[K]V)
. Sending on a closed channel panics; only the sender should close channels.
Explanation: Reading from a closed channel returns zero values; check the second boolean value to detect closure.
Question: What happens if you
defer
in a hot loop?
Answer: Each defer
allocates and adds overhead; in tight loops this can be measurable.
Explanation: Prefer explicit cleanup paths inside the loop body for performance-critical sections.