Why Use Go — Find the Bug¶
Practice finding and fixing bugs in Go code related to Go's core features and common beginner/intermediate pitfalls.
How to Use¶
- Read the buggy code carefully
- Try to find the bug without looking at the hint
- Write the fix yourself before checking the solution
- Understand why the bug happens — not just how to fix it
Difficulty Levels¶
| Level | Description |
|---|---|
| 🟢 | Easy — Common beginner Go mistakes, syntax-level bugs |
| 🟡 | Medium — Logic errors, subtle Go behavior, concurrency issues |
| 🔴 | Hard — Race conditions, memory issues, Go compiler/runtime edge cases |
Bug 1: The Missing Error Check 🟢¶
What the code should do: Read a file and print its contents.
package main
import (
"fmt"
"os"
)
func main() {
file, _ := os.Open("config.txt")
data := make([]byte, 100)
n, _ := file.Read(data)
fmt.Println(string(data[:n]))
}
Expected output:
Actual output:
Hint
What happens to `file` when `os.Open` fails? What is the zero value of a pointer?Bug Explanation
**Bug:** The error from `os.Open` is ignored using `_`. If the file does not exist, `file` is `nil`, and calling `file.Read()` on a nil pointer causes a panic. **Why it happens:** Go returns errors as values, not exceptions. Using `_` silently discards the error. **Impact:** Runtime panic — program crashes.Fixed Code
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("config.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
data := make([]byte, 100)
n, err := file.Read(data)
if err != nil {
fmt.Println("Error reading file:", err)
return
}
fmt.Println(string(data[:n]))
}
Bug 2: The Unused Import 🟢¶
What the code should do: Print the current time.
package main
import (
"fmt"
"os"
"time"
)
func main() {
now := time.Now()
fmt.Println("Current time:", now)
}
Expected output:
Actual output:
Hint
Go does not allow unused imports. Look at the import list.Bug Explanation
**Bug:** The `"os"` package is imported but never used. Go treats unused imports as compilation errors. **Why it happens:** Go enforces this to keep code clean and prevent dead imports from slowing down compilation. **Impact:** Code does not compile.Fixed Code
**What changed:** Removed the unused `"os"` import.Bug 3: Short Variable Declaration Scope 🟢¶
What the code should do: Read a value from a function and print it.
package main
import "fmt"
func getValue() (int, error) {
return 42, nil
}
func main() {
var result int
if true {
result, err := getValue()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Inside if:", result)
}
fmt.Println("Outside if:", result)
}
Expected output:
Actual output:
Hint
Look carefully at `:=` inside the `if` block. Does it assign to the outer `result` or create a new one?Bug Explanation
**Bug:** The `:=` inside the `if` block creates a **new** `result` variable that shadows the outer `result`. The outer `result` remains at its zero value (0). **Why it happens:** In Go, `:=` creates new variables. Since `err` is a new variable, `:=` also creates a new `result` in the inner scope. **Impact:** The outer `result` is never assigned — logic error.Fixed Code
**What changed:** Used `=` instead of `:=` to assign to the outer `result` variable. Declared `err` separately with `var`.Bug 4: Goroutine Loop Variable Capture 🟡¶
What the code should do: Print numbers 0 through 4 using goroutines.
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()
}
Expected output:
Actual output (Go < 1.22):
Hint
The goroutine closure captures the variable `i` by reference, not by value. By the time goroutines execute, the loop has finished.Bug Explanation
**Bug:** In Go versions before 1.22, the closure captures the loop variable `i` by reference. By the time the goroutines execute, the loop has completed and `i` equals 5. **Why it happens:** Go closures capture variables from the enclosing scope. The loop variable `i` is a single variable that is mutated each iteration. **Impact:** All goroutines print the same value (5) instead of 0-4. Note: Go 1.22+ fixed this with per-iteration loop variable scoping.Fixed Code
**What changed:** Pass `i` as a function argument to create a copy for each goroutine.Bug 5: Nil Map Write 🟡¶
What the code should do: Count word frequencies in a sentence.
package main
import (
"fmt"
"strings"
)
func wordCount(sentence string) map[string]int {
var counts map[string]int // Declared but not initialized
words := strings.Fields(sentence)
for _, word := range words {
counts[word]++ // This will panic!
}
return counts
}
func main() {
result := wordCount("go is great go is fast go is simple")
fmt.Println(result)
}
Expected output:
Actual output:
Hint
What is the zero value of a map in Go? Can you write to a nil map?Bug Explanation
**Bug:** The map `counts` is declared but never initialized. Its zero value is `nil`. Writing to a nil map causes a panic. **Why it happens:** In Go, `var m map[K]V` creates a nil map. You can read from a nil map (returns zero value), but writing panics. You must use `make(map[K]V)` to initialize it. **Impact:** Runtime panic — program crashes.Fixed Code
package main
import (
"fmt"
"strings"
)
func wordCount(sentence string) map[string]int {
counts := make(map[string]int) // Initialize the map!
words := strings.Fields(sentence)
for _, word := range words {
counts[word]++
}
return counts
}
func main() {
result := wordCount("go is great go is fast go is simple")
fmt.Println(result)
}
Bug 6: Nil Interface Trap 🟡¶
What the code should do: Return nil error when processing succeeds.
package main
import "fmt"
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
func process(input string) error {
var appErr *AppError
if input == "" {
appErr = &AppError{Code: 400, Message: "empty input"}
}
return appErr // Bug: returns non-nil interface even when appErr is nil!
}
func main() {
err := process("valid input")
if err != nil {
fmt.Println("ERROR:", err) // This executes even though processing succeeded!
} else {
fmt.Println("Success!")
}
}
Expected output:
Actual output:
Hint
A Go interface is a (type, value) pair. What is the difference between a nil interface and an interface holding a nil pointer?Bug Explanation
**Bug:** The function returns `appErr` (type `*AppError`, value `nil`). When assigned to the `error` interface, this becomes `(*AppError, nil)` — which is NOT a nil interface. A nil interface requires both type and value to be nil: `(nil, nil)`. **Why it happens:** Go's interface type is a pair of (type descriptor, data pointer). When you return a typed nil, the type descriptor is non-nil. **Impact:** The error check `err != nil` is always true, even when no error occurred.Fixed Code
package main
import "fmt"
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
func process(input string) error {
if input == "" {
return &AppError{Code: 400, Message: "empty input"}
}
return nil // Return untyped nil directly
}
func main() {
err := process("valid input")
if err != nil {
fmt.Println("ERROR:", err)
} else {
fmt.Println("Success!")
}
}
Bug 7: Deferred Call Argument Evaluation 🟡¶
What the code should do: Log the elapsed time of an operation.
package main
import (
"fmt"
"time"
)
func doWork() {
start := time.Now()
defer fmt.Printf("Elapsed: %v\n", time.Since(start)) // Bug!
// Simulate work
time.Sleep(2 * time.Second)
fmt.Println("Work done")
}
func main() {
doWork()
}
Expected output:
Actual output:
Hint
When are the arguments to a deferred function call evaluated — at the `defer` statement or when the function returns?Bug Explanation
**Bug:** Deferred function arguments are evaluated **immediately** when the `defer` statement executes, not when the deferred function runs. `time.Since(start)` is evaluated at the time of `defer`, which is right after `start` is set — so the elapsed time is nearly 0. **Why it happens:** Go spec: "Each time a defer statement executes, the function value and parameters to the call are evaluated as usual and saved anew." **Impact:** The logged elapsed time is always ~0, not the actual execution time.Fixed Code
package main
import (
"fmt"
"time"
)
func doWork() {
start := time.Now()
defer func() {
// Wrapped in closure — time.Since(start) evaluated when closure runs
fmt.Printf("Elapsed: %v\n", time.Since(start))
}()
// Simulate work
time.Sleep(2 * time.Second)
fmt.Println("Work done")
}
func main() {
doWork()
}
Bug 8: Data Race in Concurrent Counter 🔴¶
What the code should do: Safely increment a counter from multiple goroutines.
package main
import (
"fmt"
"sync"
)
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++ // Not thread-safe!
}
func (c *Counter) Value() int {
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Count:", counter.Value())
// Expected: 10000, Actual: less than 10000 (non-deterministic)
}
Expected output:
Actual output:
Hint
Run with `go run -race main.go`. What does the race detector report? What happens when two goroutines read and write `c.value` simultaneously?Bug Explanation
**Bug:** Multiple goroutines concurrently increment `c.value` without synchronization. The `++` operation is not atomic — it is read, increment, write. Two goroutines can read the same value, both increment it, and write back the same result, losing one increment. **Why it happens:** Go's memory model requires explicit synchronization for concurrent access to shared variables. **Impact:** Data race — incorrect count, non-deterministic behavior. Detected by `go run -race`. **Go spec reference:** "If a variable is accessed from multiple goroutines, the accesses must be synchronized."Fixed Code
package main
import (
"fmt"
"sync"
"sync/atomic"
)
type Counter struct {
value int64
}
func (c *Counter) Increment() {
atomic.AddInt64(&c.value, 1) // Atomic operation — thread-safe
}
func (c *Counter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Count:", counter.Value()) // Always 10000
}
Bug 9: Goroutine Leak from Unbuffered Channel 🔴¶
What the code should do: Fetch data with a timeout.
package main
import (
"fmt"
"time"
)
func fetchData() string {
time.Sleep(5 * time.Second) // Simulate slow service
return "data"
}
func fetchWithTimeout(timeout time.Duration) (string, error) {
ch := make(chan string) // Unbuffered channel
go func() {
result := fetchData()
ch <- result // This goroutine is STUCK if nobody reads from ch!
}()
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("timeout after %v", timeout)
}
}
func main() {
result, err := fetchWithTimeout(1 * time.Second)
if err != nil {
fmt.Println("Error:", err)
// The goroutine is still running and will leak!
} else {
fmt.Println("Result:", result)
}
// In production, this goroutine leak accumulates over time
time.Sleep(100 * time.Millisecond)
fmt.Println("Program exiting (goroutine still stuck in background)")
}
Expected output:
Actual output:
Hint
When the timeout fires, `fetchWithTimeout` returns. But the goroutine is still trying to send on `ch`. Since nobody will ever read from `ch`, the goroutine is stuck forever. Use a buffered channel.Bug Explanation
**Bug:** When the timeout fires, `fetchWithTimeout` returns without reading from `ch`. The goroutine that runs `fetchData()` will eventually try to send the result on the unbuffered channel, but no one is listening. The goroutine is blocked forever — a goroutine leak. **Why it happens:** Unbuffered channels block both sender and receiver. If the receiver gives up (timeout), the sender is stuck. **Impact:** Each leaked goroutine consumes ~2KB+ of memory. Over time in production, this accumulates and can cause OOM. **How to detect:** Monitor `runtime.NumGoroutine()` over time — an upward trend indicates leaks.Fixed Code
package main
import (
"fmt"
"time"
)
func fetchData() string {
time.Sleep(5 * time.Second)
return "data"
}
func fetchWithTimeout(timeout time.Duration) (string, error) {
ch := make(chan string, 1) // Buffered channel — sender won't block!
go func() {
result := fetchData()
ch <- result // Even if nobody reads, the goroutine can finish
}()
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("timeout after %v", timeout)
}
}
func main() {
result, err := fetchWithTimeout(1 * time.Second)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
Bug 10: Slice Append Side Effect 🔴¶
What the code should do: Create two different variations of a base configuration.
package main
import "fmt"
func main() {
base := make([]string, 0, 10) // Capacity 10!
base = append(base, "host=localhost")
base = append(base, "port=5432")
// Create two configurations from the same base
configA := append(base, "db=users")
configB := append(base, "db=orders")
fmt.Println("Config A:", configA)
fmt.Println("Config B:", configB)
}
Expected output:
Actual output:
Hint
Look at the capacity of `base`. When `append` does not need to grow the underlying array, it reuses the same memory. Both `configA` and `configB` share the same underlying array.Bug Explanation
**Bug:** `base` has capacity 10 but length 2. `append(base, "db=users")` writes to index 2 of the underlying array without creating a new one (capacity is sufficient). Then `append(base, "db=orders")` overwrites the same index 2. Both `configA` and `configB` point to the same underlying array. **Why it happens:** Go's `append` reuses the underlying array if there is enough capacity. Only when capacity is exceeded does it allocate a new array. **Impact:** Unexpected data corruption — both configs end up with the same value. This is a subtle bug that often appears in production code.Fixed Code
package main
import "fmt"
func main() {
base := []string{"host=localhost", "port=5432"}
// Create independent copies before appending
configA := make([]string, len(base), len(base)+1)
copy(configA, base)
configA = append(configA, "db=users")
configB := make([]string, len(base), len(base)+1)
copy(configB, base)
configB = append(configB, "db=orders")
fmt.Println("Config A:", configA)
fmt.Println("Config B:", configB)
}
Score Card¶
| Bug | Difficulty | Found without hint? | Understood why? | Fixed correctly? |
|---|---|---|---|---|
| 1 | 🟢 | ☐ | ☐ | ☐ |
| 2 | 🟢 | ☐ | ☐ | ☐ |
| 3 | 🟢 | ☐ | ☐ | ☐ |
| 4 | 🟡 | ☐ | ☐ | ☐ |
| 5 | 🟡 | ☐ | ☐ | ☐ |
| 6 | 🟡 | ☐ | ☐ | ☐ |
| 7 | 🟡 | ☐ | ☐ | ☐ |
| 8 | 🔴 | ☐ | ☐ | ☐ |
| 9 | 🔴 | ☐ | ☐ | ☐ |
| 10 | 🔴 | ☐ | ☐ | ☐ |
Rating:¶
- 10/10 without hints — Senior-level Go debugging skills
- 7-9/10 — Solid Go middle-level understanding
- 4-6/10 — Good junior, keep practicing Go
- < 4/10 — Review the topic fundamentals first