var vs := — Find the Bug¶
Practice finding and fixing bugs related to variable declarations in Go.
How to Use¶
- Read the buggy code carefully
- Try to find the bug without looking at the hint
- Fix it yourself before checking the solution
- Read the explanation to understand why the bug happens
Difficulty Levels¶
| Level | Description |
|---|---|
| Easy | Common beginner mistakes |
| Medium | Logic errors, shadowing |
| Hard | Subtle scoping, closures, concurrency |
Bug 1: Package-Level Short Declaration (Easy)¶
What the code should do: Define a package-level counter and print it.
Expected output: 1
Actual output / Error:
Hint
Where is `counter` declared? What operator can only be used inside functions?Bug Explanation & Fix
**Bug:** `:=` cannot be used at package level. It is only valid inside function bodies. **Fixed code:** **Why:** The `:=` operator is syntactic sugar that exists only at the function level. Package-level declarations must use `var`, `const`, `type`, or `func`.Bug 2: Shadowed Error (Medium)¶
What the code should do: Parse two numbers and print their sum. Return an error if either fails.
package main
import (
"fmt"
"strconv"
)
func parseAndSum(a, b string) (int, error) {
x, err := strconv.Atoi(a)
if err != nil {
return 0, err
}
y, err := strconv.Atoi(b)
if err != nil {
return 0, err
}
return x + y, nil
}
func main() {
result, err := parseAndSum("10", "abc")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Sum:", result)
}
Expected output:
Actual output: (Same as expected — this code is correct. Read carefully... the bug is in the NEXT version below.)
package main
import (
"fmt"
"strconv"
)
func parseAndSumBuggy(a, b string) (int, error) {
x, err := strconv.Atoi(a)
y, err := strconv.Atoi(b)
// BUG: err from second call overwrites err from first call
// before it is checked!
if err != nil {
return 0, err
}
_ = x
return x + y, nil
}
func main() {
result, err := parseAndSumBuggy("abc", "20")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Sum:", result)
}
Expected output:
Actual output:
Hint
What happens to the error from the first `strconv.Atoi` call before it is checked?Bug Explanation & Fix
**Bug:** The error from parsing `"abc"` (first call) is overwritten by the result of the second call (which succeeds with `"20"`). Since the second call succeeds, `err` is nil when checked. **Fixed code:** **Why:** Always check `err` immediately after the call that produces it. Do not let subsequent `:=` calls overwrite `err` before it is checked.Bug 3: Authorization Bypass via Shadowing (Medium)¶
What the code should do: Return true only if the user is an admin.
package main
import "fmt"
type User struct {
Name string
Role string
}
func isAdmin(u *User) bool {
result := false
if u != nil {
result := u.Role == "admin"
_ = result
}
return result
}
func main() {
admin := &User{Name: "Alice", Role: "admin"}
guest := &User{Name: "Bob", Role: "guest"}
fmt.Println(isAdmin(admin)) // Expected: true
fmt.Println(isAdmin(guest)) // Expected: false
fmt.Println(isAdmin(nil)) // Expected: false
}
Expected output:
Actual output:
Hint
Look at the `result :=` inside the `if` block. Is this the same `result` as the outer one?Bug Explanation & Fix
**Bug:** `result := u.Role == "admin"` inside the `if` block creates a **new** local variable `result` that shadows the outer `result`. The outer `result` remains `false` forever. **Fixed code:** **Why:** `:=` always creates a new variable in the current block scope. To update the outer `result`, use `=` (assignment, not declaration).Bug 4: Loop Variable Capture in Goroutines (Hard)¶
What the code should do: Launch 5 goroutines, each printing its own index (0-4).
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 (some order):
Actual output (Go < 1.22):
(or some combination of 4s and 5s — non-deterministic)Hint
How many variables named `i` exist? All goroutines share the same one. What is its value when the goroutines actually run?Bug Explanation & Fix
**Bug:** All goroutines capture the same `i` variable (the loop variable). By the time the goroutines execute, the for loop has completed and `i` is `5` (the value that stopped the loop). **Fix 1: Create a new variable per iteration (works all Go versions)** **Fix 2: Pass as argument** **Fix 3: Go 1.22+** — loop variables are automatically per-iteration, no fix needed. **Why:** The closure captures a reference to `i`, not a copy of its value. All goroutines share the same reference. Fix 1 creates a new variable per iteration; Fix 2 passes the current value as a function argument.Bug 5: init() Package-Level Shadowing (Hard)¶
What the code should do: Initialize a global database connection in init() and use it in main().
package main
import (
"fmt"
)
type DB struct{ name string }
func openDB(dsn string) (*DB, error) {
return &DB{name: dsn}, nil
}
var db *DB
func init() {
db, err := openDB("postgres://localhost/mydb")
if err != nil {
panic(err)
}
_ = db
}
func main() {
if db == nil {
fmt.Println("ERROR: db is nil!")
return
}
fmt.Println("Connected to:", db.name)
}
Expected output:
Actual output:
Hint
Inside `init()`, what does `db, err := ...` create? Is it the same `db` as the package-level `var db *DB`?Bug Explanation & Fix
**Bug:** `db, err := openDB(...)` inside `init()` uses `:=`, which creates a **new local** variable `db` inside `init()`. The package-level `db` remains `nil`. The `_ = db` line silences the "declared and not used" compiler error, hiding the bug. **Fixed code:** Or alternatively: **Why:** `:=` always creates new variables. To assign to an already-declared package-level variable, use `=` after declaring `err` separately with `var err error`.Bug 6: Redeclaration Compile Error (Easy)¶
What the code should do: Compute a result in two steps and print it.
package main
import "fmt"
func compute(x int) (int, error) {
return x * 2, nil
}
func main() {
result, err := compute(5)
if err != nil {
fmt.Println(err)
return
}
result, err := compute(result) // BUG
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
}
Expected output: 20
Actual output / Error:
Hint
Both `result` and `err` already exist from the first `:=`. What does `:=` require?Bug Explanation & Fix
**Bug:** On the second `result, err := compute(result)`, both `result` and `err` are already declared in the same scope. `:=` requires at least one NEW variable on the left side — there is none, so it is a compile error. **Fixed code:** **Why:** Once a variable is declared, you reassign it with `=`. `:=` is only for NEW declarations.Bug 7: Wrong Type Inferred (Medium)¶
What the code should do: Store a duration and use it for sleeping.
package main
import (
"fmt"
"time"
)
func main() {
delay := 5 // BUG: what type is this?
fmt.Println("Waiting...")
time.Sleep(time.Duration(delay)) // this will sleep for 5 nanoseconds!
fmt.Println("Done")
}
Expected behavior: Sleep for 5 seconds. Actual behavior: Sleeps for 5 nanoseconds (imperceptibly fast).
Hint
What type does `delay := 5` create? What unit does `time.Duration` use?Bug Explanation & Fix
**Bug:** `delay := 5` infers type `int`. When converted to `time.Duration`, `5` means 5 nanoseconds (since `time.Duration` is `int64` measured in nanoseconds). So the sleep is 5ns, not 5 seconds. **Fixed code (option 1 — explicit type):** **Fixed code (option 2 — inline):** **Fixed code (option 3 — direct):** **Why:** The short declaration `:=` infers `int` from the literal `5`. When working with `time.Duration`, always use `time.Second`, `time.Millisecond`, etc., or declare with `var delay time.Duration = ...` to get the correct type.Bug 8: Nil Map Panic (Medium)¶
What the code should do: Count word frequencies in a string.
package main
import (
"fmt"
"strings"
)
func wordCount(text string) map[string]int {
var counts map[string]int // nil map
for _, word := range strings.Fields(text) {
counts[word]++ // BUG: writing to nil map panics!
}
return counts
}
func main() {
freq := wordCount("the quick brown fox jumps over the lazy dog the")
fmt.Println(freq["the"]) // Expected: 3
}
Expected output: 3
Actual output:
Hint
What is the zero value of a map? Can you write to a nil map?Bug Explanation & Fix
**Bug:** `var counts map[string]int` creates a `nil` map. In Go, reading from a nil map returns the zero value (safe), but **writing to a nil map panics**. **Fixed code (option 1 — initialize with make):** **Fixed code (option 2 — lazy init):** **Why:** Maps must be initialized with `make` before writing. Use `make(map[K]V)` or `make(map[K]V, hint)` to create a writable map. `var m map[K]V` creates a nil map — safe for reads, panics on writes.Bug 9: Defer in Loop Closes Wrong File (Hard)¶
What the code should do: Open and process multiple files, closing each after processing.
package main
import (
"fmt"
"os"
)
func processFiles(names []string) error {
for _, name := range names {
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close() // BUG: all defers run at function end!
// process file...
fmt.Println("Processing:", name)
}
return nil
}
func main() {
processFiles([]string{"a.txt", "b.txt", "c.txt"})
// Files are closed here, not after each iteration
// This leaks file descriptors during the loop
}
Problem: File descriptors are not released until processFiles returns. For large lists of files, this can exhaust the OS file descriptor limit.
Hint
`defer` in a loop defers until the surrounding *function* returns, not until the loop iteration ends. How can you ensure each file is closed after its iteration?Bug Explanation & Fix
**Bug:** `defer f.Close()` inside a loop schedules all close calls to run when `processFiles` returns — not when each loop iteration ends. If processing 10,000 files, all 10,000 are open simultaneously. **Fix 1: Wrap in anonymous function**func processFilesFixed(names []string) error {
for _, name := range names {
if err := processOneFile(name); err != nil {
return err
}
}
return nil
}
func processOneFile(name string) error {
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close() // now deferred to processOneFile's return
fmt.Println("Processing:", name)
return nil
}
Bug 10: Interface Nil Trap (Hard)¶
What the code should do: Return an error if validation fails, nil if it passes.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func validateAge(age int) error {
var err *ValidationError // nil pointer of type *ValidationError
if age < 0 || age > 150 {
err = &ValidationError{Message: "age out of range"}
}
return err // BUG: returns non-nil interface even when err is nil!
}
func main() {
err := validateAge(25)
if err != nil {
fmt.Println("Invalid:", err)
} else {
fmt.Println("Valid age") // Expected: this should print
}
}
Expected output: Valid age
Actual output: Invalid: <nil> or unexpected nil-check result.
Hint
What is the difference between a nil `*ValidationError` and a nil `error` interface? When you return a typed nil pointer as an interface, is the interface nil?Bug Explanation & Fix
**Bug:** In Go, an interface value is nil only if both its type and value are nil. When you return `err` (which is `(*ValidationError)(nil)` — a non-nil type, nil value), the `error` interface is NOT nil even though the pointer is nil. **Fixed code (option 1 — return untyped nil)** **Fixed code (option 2 — keep var but change return)** **Why:** An interface value consists of (type, value) pair. A `(*ValidationError)(nil)` has type `*ValidationError` and value `nil` — the interface itself is non-nil. Only `return nil` (without a type) returns a truly nil interface.Bug 11: Wrong Variable Updated in Select (Hard)¶
What the code should do: Receive from two channels and store the last received value.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "from ch1"
ch2 <- "from ch2"
var lastMsg string
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
lastMsg := msg // BUG
_ = lastMsg
case msg := <-ch2:
lastMsg := msg // BUG
_ = lastMsg
}
}
fmt.Println("Last message:", lastMsg)
_ = time.Second
}
Expected output: Last message: from ch2 (or from ch1 depending on timing)
Actual output: Last message: (empty string — lastMsg never updated)
Hint
Look at `lastMsg := msg` inside the select cases. Where does `lastMsg` live?Bug Explanation & Fix
**Bug:** `lastMsg := msg` inside each `case` creates a NEW local `lastMsg` that shadows the outer `lastMsg`. The outer variable declared with `var lastMsg string` is never updated. **Fixed code:** **Why:** `:=` inside a `case` block creates a new scope variable. To update the outer `lastMsg`, use `=` (simple assignment).Bug 12: Uninitialized Receiver in Method (Medium)¶
What the code should do: Use a counter that tracks its own call count.
package main
import "fmt"
type Counter struct {
value int
calls int
}
func (c Counter) Increment() { // BUG: value receiver
c.value++
c.calls++
fmt.Printf("incremented to %d (call %d)\n", c.value, c.calls)
}
func main() {
var c Counter
c.Increment()
c.Increment()
c.Increment()
fmt.Println("Final value:", c.value) // Expected: 3
}
Expected output:
Actual output: