Go Functions Basics — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code related to function declarations, calls, parameters, returns, or defer. Identify the bug, explain why it occurs, and provide the corrected code. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Missing Return¶
package main
import "fmt"
func absolute(n int) int {
if n < 0 {
return -n
}
}
func main() {
fmt.Println(absolute(-5))
}
What is the bug?
Hint
What does the compiler require for every code path of a function with a result type?Solution
**Bug**: The function returns `-n` only when `n < 0`. If `n >= 0`, control falls off the end of the function with no `return` — a **compile error**: `missing return at end of function`. **Fix**: **Key lesson**: Every code path of a non-void function must end in a terminating statement (`return`, `panic`, `os.Exit`, infinite loop, or labeled goto). The compiler does NOT analyze whether a path is logically reachable.Bug 2 🟢 — Wrong Return Type¶
package main
import "fmt"
func ageNextYear(age int) string {
return age + 1
}
func main() {
fmt.Println(ageNextYear(29))
}
What is the bug?
Hint
The declared return type vs. the type of the returned expression.Solution
**Bug**: `age + 1` is an `int`, but the function declares its return type as `string`. **Compile error**: `cannot use age + 1 (untyped int constant 1 + age) as string in return statement`. **Fix** (option A — convert to string): **Fix** (option B — change return type): **Key lesson**: Go has no implicit conversion between numeric and string types — even between numeric types like `int` and `int64`. Use `strconv.Itoa`, `fmt.Sprintf`, or explicit conversions.Bug 3 🟢 — Trying to Mutate a Pass-by-Value Argument¶
package main
import "fmt"
func setToZero(x int) {
x = 0
}
func main() {
n := 42
setToZero(n)
fmt.Println(n) // expected 0
}
What is the bug?
Hint
How does Go pass arguments — by value or by reference?Solution
**Bug**: Go passes arguments **by value**. `setToZero` receives a copy of `n`. Modifying the copy does not affect the caller's variable. Output: `42`, not `0`. **Fix** (use a pointer): **Key lesson**: Every parameter in Go is a local copy. To allow mutation by the callee, pass a pointer. This rule applies even to slices and maps (their headers are copied; the data is shared — see 2.7.3).Bug 4 🟢 — Two Functions With the Same Name¶
package main
import "fmt"
func add(a, b int) int { return a + b }
func add(a, b float64) float64 { return a + b }
func main() {
fmt.Println(add(1, 2))
fmt.Println(add(1.5, 2.5))
}
What is the bug?
Hint
Does Go allow function overloading?Solution
**Bug**: Go does NOT support function overloading. Two top-level functions in the same package cannot share a name. **Compile error**: `add redeclared in this block`. **Fix** (option A — distinct names): **Fix** (option B — generics, Go 1.18+): **Key lesson**: Each function name in a package must be unique. Use distinct names, generics, or interfaces to express polymorphism.Bug 5 🟡 — defer Argument Evaluated Eagerly¶
The author expected i = 99 to print. What actually prints?
Hint
When does `defer` evaluate its arguments — at the time of the `defer` statement, or at the time of the deferred call?Solution
**Bug**: `defer` evaluates the **arguments** of the call at the moment the `defer` statement executes, not when the deferred call runs. So `i` is captured as `1`. The deferred `fmt.Println` runs at function exit and prints `i = 1`. Output: **Fix** (defer a closure that captures `i` by reference): **Key lesson**: With `defer call(args)`, **args are eager, the call is lazy**. Wrap in a closure when you want late evaluation.Bug 6 🟡 — defer Inside a Loop Holds Resources Too Long¶
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 f.Close() // BUG
// ... process f ...
_ = f
}
return nil
}
func main() {
_ = processFiles([]string{"a.txt", "b.txt"})
fmt.Println("done")
}
What is the bug?
Hint
When does each deferred `f.Close()` actually run? What if `paths` has 10 000 entries?Solution
**Bug**: `defer` runs at **function exit**, not at the end of each loop iteration. Every `f.Close()` is queued and runs only after the loop and the entire function complete. With many files, all file descriptors stay open simultaneously — easy to hit the OS limit (`EMFILE`, "too many open files"). Additionally, the deferred records consume memory across iterations (open-coded defer optimization is disabled inside loops). **Fix** — extract a helper so each defer scope is per-file:func processFiles(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs at end of processOne — once per file
// ... process f ...
_ = f
return nil
}
Bug 7 🟡 — Calling a nil Function Variable¶
package main
import "fmt"
type Handler func(string)
var handlers = map[string]Handler{
"greet": func(name string) { fmt.Println("hi", name) },
}
func dispatch(event string, payload string) {
h := handlers[event]
h(payload)
}
func main() {
dispatch("greet", "Ada")
dispatch("unknown", "Linus") // BUG
}
What is the bug?
Hint
What is the zero value of a function type? What happens when you look up a missing key in a map?Solution
**Bug**: `handlers["unknown"]` returns the zero value for `Handler`, which is `nil`. Calling `nil(payload)` panics: `runtime error: invalid memory address or nil pointer dereference`. **Fix** — check before calling, or use comma-ok: Or with a nil check: **Key lesson**: A map lookup of a missing key returns the zero value of the value type silently. For function-typed maps, this means `nil`. Always check before invoking.Bug 8 🟡 — Method Value Captures the Receiver Wrong¶
package main
import "fmt"
type Counter struct{ n int }
func (c Counter) Show() { fmt.Println(c.n) }
func main() {
c := Counter{n: 1}
show := c.Show
c.n = 99
show() // expected 99
}
What is the bug?
Hint
The receiver of `Show` is a **value**, not a pointer. What does the method value capture?Solution
**Bug**: `Show` has a **value receiver**. `c.Show` is a method value that captures a **copy** of `c` at the moment of the binding (`c.n == 1`). Subsequent modifications to `c.n` are not visible to `show`. Output: `1`. **Fix** (option A — pointer receiver): **Fix** (option B — call the method directly each time, instead of binding): **Key lesson**: Method values bound to value receivers freeze a snapshot. If you need to see live updates, use a pointer receiver.Bug 9 🔴 — Init Order Trap¶
config.go:
package main
import "fmt"
var Threshold = computeThreshold()
func computeThreshold() int {
fmt.Println("computing threshold")
return Multiplier * 10
}
var Multiplier = 5
main.go:
The author expects Threshold to be 50. What does it actually compute?
Hint
In what order are package-level variables initialized?Solution
**Bug**: Package-level variables are initialized in **dependency order** (variables a variable depends on are initialized first). Here, `Threshold` depends on `computeThreshold()`, which depends on `Multiplier`. Go correctly orders `Multiplier` before `Threshold` based on this dependency. **However**, if you remove the use of `Multiplier` from `computeThreshold` (or reference it indirectly via reflection), Go falls back to source-declaration order. In *that* case, `Multiplier` would still be `0` (its zero value) when `computeThreshold` runs, producing `Threshold = 0`. In this particular code as written, output is: because Go's dependency analysis catches the reference. But the **bug pattern** is real: if the dependency is hidden behind a function or interface that the analyzer cannot trace, you get unexpected zero values. **Robust fix** — use `init()` for ordering-sensitive setup: Or move the constant inline: **Key lesson**: Package variable initialization is dependency-driven, but only via *direct, visible* references. Hidden dependencies (through interfaces, reflection, function values) can produce zero-value bugs. Use `init()` or `sync.Once` when ordering matters.Bug 10 🔴 — Closure Capturing Loop Variable in Goroutine (Pre-1.22 Behavior)¶
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var results = make([]int, 5)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = i * i
}()
}
wg.Wait()
fmt.Println(results)
}
The author expected [0 1 4 9 16]. What can actually happen?
Hint
What does Go 1.22's loop-variable semantic change cover, and what does it NOT cover?Solution
**Bug**: This is a C-style `for` loop (not `for range`). The Go 1.22 per-iteration loop variable change applies **only to `for range`**. C-style `for` still shares one `i` variable across all iterations. The goroutines may all see `i == 5` by the time they execute, producing `index out of range` panics on `results[5]`. Even if they hit valid indices, results are non-deterministic and you have a data race on `i`. **Fix** (option A — pass `i` as an argument): **Fix** (option B — shadow `i`): **Fix** (option C — use `for range`, which IS per-iteration in Go 1.22+): **Key lesson**: Go 1.22's per-iteration loop variable change applies to `for range`, NOT to C-style `for i := 0; ...; i++`. The classic capture-by-shadow or capture-by-arg fixes still apply to C-style loops in all Go versions.Bug 11 🔴 — Recover Outside a Deferred Function¶
package main
import "fmt"
func mayPanic() {
panic("boom")
}
func safe() {
defer fmt.Println("after panic")
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
mayPanic()
}
func main() {
safe()
fmt.Println("survived")
}
The author expected safe to recover. What actually happens?
Hint
What is the exact requirement for `recover` to stop a panic?Solution
**Bug**: `recover()` only stops a panic when called **directly inside a deferred function**. Here, `recover` is called **before** `mayPanic`, in the normal execution path. At that moment there is no panic in progress, so `recover()` returns nil. The panic from `mayPanic()` then propagates and crashes the program. The deferred `fmt.Println("after panic")` does run during unwinding, so you see: **Fix** — wrap `recover` in a deferred closure: Now: when `mayPanic` panics, the deferred function runs during unwinding, `recover()` catches the panic, and `safe` returns normally. **Key lesson**: `recover` is a "magic" builtin only effective inside a deferred function during a panic. Always pair it with `defer func() { ... recover() ... }()`.Bug 12 🔴 — Closure Holding a Large Object Alive¶
package main
import (
"fmt"
"runtime"
)
type BigBlob struct {
data [1 << 20]byte // 1 MiB
}
func makeReporter(b *BigBlob) func() int {
return func() int {
return int(b.data[0])
}
}
func main() {
var fns []func() int
for i := 0; i < 100; i++ {
b := &BigBlob{}
fns = append(fns, makeReporter(b))
}
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("Heap: %d MB\n", ms.HeapAlloc/(1024*1024))
_ = fns
}
The author expected the heap to be near zero after runtime.GC(). Instead, it shows ~100 MB. Why?
Hint
What does each closure in `fns` capture? Can the GC free the `BigBlob`s?Solution
**Bug**: Each closure returned by `makeReporter` captures `b` (the `*BigBlob`). Because `fns` keeps each closure alive, each closure keeps its `*BigBlob` reachable. The GC cannot free any of the 100 `BigBlob` instances — total ~100 MB retained. **Fix** (option A — capture only what you need): After this fix, the closure captures only an `int`. The `BigBlob` becomes unreachable as soon as `makeReporter` returns and is collected. **Fix** (option B — clear the reference once you don't need the closure): **Key lesson**: Closures capture variables by reference (semantically, by funcval slot). Capturing a pointer to a large object keeps that object alive for the closure's lifetime. Always capture the **minimum** state required.Bug 13 🔴 — recover in a Goroutine Doesn't Save the Parent¶
package main
import (
"fmt"
"sync"
)
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("main recovered:", r)
}
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
panic("worker failed")
}()
wg.Wait()
fmt.Println("main exiting")
}
The author expected main recovered: worker failed. What actually happens?
Hint
A panic in goroutine A — does goroutine B's deferred `recover` see it?Solution
**Bug**: Each goroutine's panic is **independent**. The `recover` in `main`'s deferred function only catches panics that happen *in `main`'s call stack*. A panic in another goroutine cannot be recovered from `main`. The worker goroutine panics, the runtime crashes the entire program, and main's recover never runs. **Fix** — recover **inside** the goroutine that may panic: For composability, wrap the goroutine body: **Key lesson**: Panic / recover is goroutine-local. Each goroutine that may panic must recover for itself, or the entire program crashes. There is no parent-goroutine try/catch.Bonus Bug 🔴 — Returning a Pointer to a Local Slice Element¶
package main
import "fmt"
func firstPointer() *int {
s := []int{10, 20, 30}
return &s[0]
}
func main() {
p := firstPointer()
fmt.Println(*p) // expected 10
// ... but is it safe?
}
Is the bug safety or performance?