Go Anonymous Functions — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code involving anonymous functions or closures. Identify the bug, explain why it occurs, and provide the corrected code. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Defer Without Invocation¶
package main
import "fmt"
func main() {
defer func() {
fmt.Println("cleanup")
}
fmt.Println("main")
}
What is the bug?
Hint
What kind of expression does `defer` expect?Solution
**Bug**: `defer func() { ... }` is a syntax/semantic error — `defer` requires a function CALL expression, not a function value. The trailing `()` is missing. **Compile error**: `defer requires function call, not function value`. **Fix**: **Key lesson**: Always end an anonymous-function defer with `()` to make it a call.Bug 2 🟢 — Recursion By Name¶
package main
import "fmt"
func main() {
fact := func(n int) int {
if n <= 1 {
return 1
}
return n * fact(n-1)
}
fmt.Println(fact(5))
}
What is the bug?
Hint
Inside the literal body, when does `fact` become defined?Solution
**Bug**: `fact` is being declared by the `:=` assignment. The literal on the right side is type-checked and compiled BEFORE `fact` is in scope. **Compile error**: `undefined: fact` (or "fact declared but not used" on the inner reference). **Fix** — declare the variable first, then assign: This works because the variable `fact` exists when the literal references it; the captured pointer/value is updated by the assignment before the call. **Key lesson**: Anonymous functions can't recurse by name directly. Use `var fact ...; fact = ...` to enable recursion.Bug 3 🟢 — Loop Variable in Goroutine (Pre-1.22)¶
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i)
}()
}
wg.Wait()
}
(Assume go.mod declares go 1.21 or earlier.)
What does this print?
Hint
What value of `i` does each goroutine see in pre-1.22 Go?Solution
**Bug**: All goroutines capture the SAME `i` variable. By the time most run, the loop has finished and `i == 5`. Output (often): **Fix** (option A — pass as argument): **Fix** (option B — shadow with `i := i`): **Fix** (option C — upgrade to Go 1.22): Update `go.mod` to `go 1.22`. Each iteration creates a fresh `i`, and the original code works. **Key lesson**: Pre-1.22 Go shares loop variables across iterations. Goroutines capturing them see the final value. Pass as argument or shadow.Bug 4 🟢 — IIFE Without Result¶
package main
import "fmt"
func main() {
result := func(a, b int) int { return a + b }(3, 4)
fmt.Println(result)
// What about this?
result2 := func() {
fmt.Println("hello")
}()
fmt.Println(result2)
}
What is the bug?
Hint
What does `result2` get assigned?Solution
**Bug**: The second IIFE returns no values. **Compile error**: `result2 declared but not used` and/or `func() { ... }() used as value`. **Fix** (option A — make the IIFE return a value): **Fix** (option B — call the IIFE for its side effect, no assignment): **Key lesson**: IIFE returns whatever the literal returns. If it returns nothing, you can't assign the result.Bug 5 🟡 — Closure Captures Loop Variable in Slice of Closures¶
package main
import "fmt"
func main() {
fns := []func() int{}
for i := 0; i < 3; i++ {
fns = append(fns, func() int {
return i * 10
})
}
for _, f := range fns {
fmt.Println(f())
}
}
In Go 1.21, what does this print? In Go 1.22+?
Hint
The 1.22 change applies to all loop forms when iteration var is `:=`.Solution
**Pre Go 1.22**: All closures share the same `i`. After the loop, `i == 3`. Output: **Go 1.22+**: Each iteration creates a fresh `i`. Output: **Fix for pre-1.22** — shadow: Or pass as a factory argument: **Key lesson**: Closure capture interacts with loop semantics. Behavior changed in Go 1.22.Bug 6 🟡 — Heavy Capture Pinning Memory¶
package main
import "fmt"
type BigData struct {
buf [1 << 20]byte // 1 MB
}
func makeReader(b *BigData) func() byte {
return func() byte {
return b.buf[0]
}
}
func main() {
var fns []func() byte
for i := 0; i < 100; i++ {
b := &BigData{}
fns = append(fns, makeReader(b))
}
fmt.Println("total functions:", len(fns))
// Each closure pins 1 MB
}
What is the bug?
Hint
What does each closure capture? How long does it live?Solution
**Bug**: Each closure captures `b` (a `*BigData`). All 100 BigData instances stay alive as long as the closures exist. Total: ~100 MB pinned. **Fix** — capture only what you need: Now each closure captures 1 byte; the BigData instances are eligible for GC after `makeReader` returns. **Key lesson**: Closures capture by reference; capturing a pointer keeps the entire pointee alive. Extract only what's needed.Bug 7 🟡 — Defer Inside Loop¶
package main
import (
"fmt"
"os"
)
func processFiles(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer func() {
f.Close()
fmt.Println("closed", p)
}()
// ... read f ...
}
return nil
}
What is the bug?
Hint
When does each deferred close run?Solution
**Bug**: Each `defer` runs at function exit, not loop-iteration exit. For 1000 files, all 1000 file descriptors stay open until the loop completes — easy to hit `EMFILE` ("too many open files"). Also: each iteration's closure captures `f` and `p`. With pre-1.22 Go, `p` is shared across iterations and may show the final value. **Fix** — extract a per-iteration helper:func processFiles(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(p string) error {
f, err := os.Open(p)
if err != nil { return err }
defer func() {
f.Close()
fmt.Println("closed", p)
}()
// ... read f ...
return nil
}
Bug 8 🟡 — Closure Variable Mutation¶
package main
import "fmt"
func main() {
counter := func() func() int {
n := 0
return func() int {
n = n + 1
return n
}
}()
for i := 0; i < 3; i++ {
fmt.Println(counter())
}
}
This prints 1, 2, 3 — but is there a hidden race condition if used concurrently?
Hint
What if multiple goroutines call counter concurrently?Solution
**Bug (latent)**: The counter captures `n` and increments it without synchronization. Concurrent calls cause a data race. `go test -race` would flag this. **Fix** — use atomic operations: Or use a mutex: **Key lesson**: Closures capture by reference. Concurrent access to captured mutable state requires synchronization, just like any shared variable.Bug 9 🔴 — Closure-Caused Goroutine Leak¶
package main
import (
"fmt"
"time"
)
func startWorker() {
bigData := make([]byte, 1<<20)
go func() {
for {
time.Sleep(10 * time.Second)
_ = bigData
}
}()
}
func main() {
for i := 0; i < 100; i++ {
startWorker()
}
fmt.Println("started 100 workers")
select {} // block forever
}
What's wrong?
Hint
Do the goroutines ever exit? What does each one keep alive?Solution
**Bug**: 100 goroutines, each capturing 1 MB of `bigData`, never exit. Total pinned memory: 100 MB. The program "leaks" goroutines and memory linearly. **Fix** — make the goroutine respect cancellation: Or extract minimal data to capture: **Key lesson**: Goroutines that never exit are leaks. Combined with closures capturing large data, they can pin gigabytes. Always design goroutines for cancellation.Bug 10 🔴 — Method Value Captures Stale Receiver¶
package main
import "fmt"
type S struct{ v int }
func (s S) Show() { fmt.Println(s.v) }
func main() {
s := S{v: 1}
show := s.Show
s.v = 99
show()
}
The author expected 99. What prints?
Hint
Is the receiver of `Show` a value or pointer?Solution
**Bug**: `Show` has a value receiver. `s.Show` creates a method value that captures a COPY of `s` at the time of binding (when `s.v == 1`). Later mutation to `s.v` doesn't affect the captured copy. Output: `1`. **Fix** (option A — pointer receiver): **Fix** (option B — call the method directly each time, no binding): **Key lesson**: Method values bound to value receivers capture a snapshot. Use pointer receivers for live-state behavior, or call the method directly.Bug 11 🔴 — Anonymous Function Allocates Per Iteration¶
package main
import "fmt"
func process(items []int, transform func(int) int) []int {
out := make([]int, len(items))
for i, x := range items {
out[i] = transform(x)
}
return out
}
func main() {
items := []int{1, 2, 3, 4, 5}
for i := 0; i < 1000; i++ {
result := process(items, func(n int) int { return n * 2 })
_ = result
}
fmt.Println("done")
}
The anonymous function is created 1000 times. Is this wasteful?
Hint
Does the literal capture anything?Solution
**Discussion**: The literal captures nothing. Non-capturing function literals are essentially free — the compiler emits a single shared funcval per literal, hoisted to a global. There's no per-iteration allocation. But if the literal captured something:for i := 0; i < 1000; i++ {
factor := i
result := process(items, func(n int) int { return n * factor })
_ = result
}
Bug 12 🔴 — Closure Returned From Loop in Map¶
package main
import "fmt"
func main() {
handlers := map[string]func() string{}
names := []string{"a", "b", "c"}
for _, name := range names {
handlers[name] = func() string {
return "handler for " + name
}
}
for _, name := range names {
fmt.Println(handlers[name]())
}
}
In Go 1.21, what does this print? In Go 1.22+?
Hint
Same loop-variable capture issue as Bug 5 but with `for ... range`.Solution
**Pre Go 1.22**: All closures share the same `name` variable. After the loop, `name == "c"`. Output: **Go 1.22+**: Each iteration creates a fresh `name`. Output: **Fix for pre-1.22** — shadow: **Key lesson**: The Go 1.22 change applies to `for ... range` (and all loop forms with `:=` iteration vars). Both for-clause and range-clause loops create per-iteration variables now.Bonus Bug 🔴 — Defer Closure in Recursive Function¶
package main
import "fmt"
func deepCall(n int) {
defer func() {
fmt.Println("returning from", n)
}()
if n > 0 {
deepCall(n - 1)
}
}
func main() {
deepCall(3)
}
What does this print? Is there a performance concern?