Go if-else — Find the Bug¶
Each bug has a difficulty rating: - 🟢 Easy — Obvious once spotted - 🟡 Medium — Requires understanding of Go semantics - 🔴 Hard — Subtle runtime behavior or compiler edge case
Bug 1 🟢 — Wrong Comparison Operator¶
package main
import "fmt"
func isAdult(age int) bool {
if age > 18 {
return true
}
return false
}
func main() {
fmt.Println(isAdult(18)) // Expected: true, Got: false
fmt.Println(isAdult(19)) // Expected: true, Got: true
fmt.Println(isAdult(17)) // Expected: false, Got: false
}
Hint
Think about what happens when age is exactly 18. Is someone 18 years old an adult?Solution
**Bug:** `age > 18` should be `age >= 18`. The current code excludes 18-year-olds. **Root cause:** Off-by-one error. The condition should be "greater than or equal to" to include the boundary value 18. **Lesson:** Always double-check `>` vs `>=` and `<` vs `<=` at boundaries. This is one of the most common bugs in conditional logic.Bug 2 🟢 — Wrong Order of Conditions¶
package main
import "fmt"
func getGrade(score int) string {
if score >= 60 {
return "D"
} else if score >= 70 {
return "C"
} else if score >= 80 {
return "B"
} else if score >= 90 {
return "A"
}
return "F"
}
func main() {
fmt.Println(getGrade(95)) // Expected: A, Got: D
fmt.Println(getGrade(82)) // Expected: B, Got: D
fmt.Println(getGrade(45)) // Expected: F, Got: F
}
Hint
Go evaluates conditions top-to-bottom and takes the FIRST matching branch. What matches 95 first?Solution
**Bug:** Conditions are in the wrong order. `score >= 60` matches first for any score 60+, so scores of 70, 80, 90+ always return "D". **Root cause:** Range-based if-else chains must check the most restrictive condition first. **Lesson:** When using `>=` in a chain, order from highest to lowest threshold.Bug 3 🟢 — Missing else (FizzBuzz)¶
package main
import "fmt"
func fizzBuzz(n int) string {
if n%3 == 0 {
return "Fizz"
}
if n%5 == 0 {
return "Buzz"
}
if n%3 == 0 && n%5 == 0 {
return "FizzBuzz"
}
return fmt.Sprintf("%d", n)
}
func main() {
fmt.Println(fizzBuzz(15)) // Expected: FizzBuzz, Got: Fizz
fmt.Println(fizzBuzz(3)) // Expected: Fizz, Got: Fizz
fmt.Println(fizzBuzz(5)) // Expected: Buzz, Got: Buzz
}
Hint
For n=15: it's divisible by both 3 and 5. Which check runs first?Solution
**Bug:** The "FizzBuzz" condition is checked last, but numbers divisible by both 3 and 5 will match the first condition (`n%3 == 0`) and return "Fizz" before reaching "FizzBuzz". **Root cause:** The most specific condition (divisible by both) must come before the less specific ones. **Lesson:** Always handle the most specific case before the more general cases.Bug 4 🟡 — Typed Nil Interface¶
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 getError(fail bool) error {
var err *AppError
if fail {
err = &AppError{404, "not found"}
}
return err // BUG: returns typed nil when fail=false
}
func main() {
err := getError(false)
if err != nil {
fmt.Println("Error occurred:", err) // This prints unexpectedly!
} else {
fmt.Println("No error")
}
}
Hint
In Go, an interface holds two things: a type and a value. What type does `err` have when `fail=false`?Solution
**Bug:** `var err *AppError` creates a typed nil. When returned as `error` interface, it becomes `(*AppError)(nil)` — a non-nil interface containing a nil pointer. An interface is nil only when BOTH the type AND value are nil. `(*AppError)(nil)` has a non-nil type. **Root cause:** Typed nil vs untyped nil — one of Go's most notorious gotchas. **Detection:** Run `go vet` or use `errcheck` linter. Also: never declare `var err ConcreteType` and return it as an interface.Bug 5 🟡 — Shadowed Variable in if Init¶
package main
import (
"fmt"
"strconv"
)
func processNumbers(inputs []string) ([]int, []string) {
var results []int
var errors []string
for _, input := range inputs {
if n, err := strconv.Atoi(input); err != nil {
errors = append(errors, err.Error())
} else {
results = append(results, n*2)
}
// BUG: can you spot it?
fmt.Println("Processing:", input) // This is fine
}
return results, errors
}
func main() {
results, errs := processNumbers([]string{"1", "abc", "3"})
fmt.Println("Results:", results) // Expected: [2, 6], Got: [2, 6]
fmt.Println("Errors:", errs) // Expected: 1 error, Got: 1 error
// The bug is subtle — look at a different version:
processV2([]string{"1", "abc", "3"})
}
func processV2(inputs []string) {
total := 0
for _, input := range inputs {
if n, err := strconv.Atoi(input); err == nil {
total = total + n // using n from init
}
// BUG: What if we try to use n here?
// n is out of scope!
// fmt.Println(n) // compile error
}
fmt.Println("Total:", total)
}
Hint
This bug is about variable scope. What happens when you declare variables in the init statement that you want to use after the if-else block?Solution
**Bug (conceptual):** Variables declared in the if-init statement (`n, err`) are scoped only to the if-else block. If you need them after the block, you must declare them outside.func processV3(inputs []string) {
total := 0
for _, input := range inputs {
// Option 1: Declare outside if you need it after
n, err := strconv.Atoi(input)
if err == nil {
total += n
fmt.Println("parsed:", n) // n accessible here
}
// n is accessible here too
fmt.Printf("processed %q -> n=%d, err=%v\n", input, n, err)
}
fmt.Println("Total:", total)
}
Bug 6 🟡 — Short-Circuit Not Used for Safety¶
package main
import "fmt"
type Config struct {
Settings map[string]string
}
func getValue(cfg *Config, key string) string {
if cfg.Settings[key] != "" { // BUG: panics when cfg is nil
return cfg.Settings[key]
}
return "default"
}
func main() {
fmt.Println(getValue(nil, "theme")) // PANIC: nil pointer dereference
}
Hint
What happens when you access a field on a nil pointer? How can short-circuit evaluation help?Solution
**Bug:** When `cfg` is nil, `cfg.Settings` causes a nil pointer dereference panic. The nil check must come before accessing `cfg.Settings`.func getValue(cfg *Config, key string) string {
if cfg != nil && cfg.Settings[key] != "" {
return cfg.Settings[key]
}
return "default"
}
// Even better with guard clause:
func getValueSafe(cfg *Config, key string) string {
if cfg == nil {
return "default"
}
if val := cfg.Settings[key]; val != "" {
return val
}
return "default"
}
Bug 7 🟡 — Float Comparison in Condition¶
package main
import "fmt"
func calculateTax(amount float64) float64 {
if amount == 100.0 { // BUG: may never be true for computed floats
return 10.0
}
return amount * 0.1
}
func main() {
subtotal := 33.33 + 33.33 + 33.34
fmt.Printf("subtotal = %.10f\n", subtotal) // May not be exactly 100.0
tax := calculateTax(subtotal)
fmt.Printf("tax = %.2f\n", tax)
// The problem:
a := 0.1 + 0.2
if a == 0.3 {
fmt.Println("equal") // NEVER prints due to floating point!
}
fmt.Printf("0.1 + 0.2 = %.20f\n", a)
}
Hint
Floating point arithmetic is not exact. 0.1 + 0.2 in binary floating point is not exactly 0.3.Solution
**Bug:** Comparing floating-point numbers with `==` is almost always wrong unless the value comes directly from a literal or a deterministic computation.import "math"
const epsilon = 1e-9
func floatEqual(a, b float64) bool {
return math.Abs(a-b) < epsilon
}
func calculateTax(amount float64) float64 {
if floatEqual(amount, 100.0) {
return 10.0
}
return amount * 0.1
}
// For currency, use integer cents or decimal library
// import "github.com/shopspring/decimal"
Bug 8 🔴 — Race Condition in if-else Condition¶
package main
import (
"fmt"
"sync"
"time"
)
type Cache struct {
data map[string]string
mu sync.Mutex
}
func (c *Cache) GetOrFetch(key string) string {
// BUG: check and set are not atomic!
if _, ok := c.data[key]; !ok { // Thread 1 checks here
// Thread 2 also checks here — both see "not found"
time.Sleep(time.Millisecond) // simulate fetch latency
c.mu.Lock()
c.data[key] = "fetched-" + key // Both threads write!
c.mu.Unlock()
}
return c.data[key]
}
func main() {
cache := &Cache{data: make(map[string]string)}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cache.GetOrFetch("key1")
}()
}
wg.Wait()
fmt.Println("Done:", cache.data["key1"])
}
// Also has DATA RACE: reading c.data without lock!
// Run: go test -race to detect
Hint
The map read (`c.data[key]`) outside the mutex is a data race. And even with the mutex, check-then-act is a TOCTOU bug.Solution
**Bug 1:** Reading `c.data[key]` outside mutex is a data race. **Bug 2:** Even with locks, the check and the write are not atomic (TOCTOU — Time of Check, Time of Use).func (c *Cache) GetOrFetch(key string) string {
// Double-checked locking pattern
c.mu.Lock()
defer c.mu.Unlock()
if val, ok := c.data[key]; ok {
return val
}
// Fetch and store while holding lock
val := "fetched-" + key
c.data[key] = val
return val
}
// For high concurrency, use sync.Map or singleflight:
// import "golang.org/x/sync/singleflight"
Bug 9 🔴 — else Block with Variable Declared in if¶
package main
import (
"fmt"
"os"
)
func readConfig() error {
if file, err := os.Open("config.json"); err != nil {
return fmt.Errorf("cannot open config: %w", err)
}
// BUG: file is not closed! Where is defer file.Close()?
// Also: file is out of scope here!
// Trying to use file here would be a compile error
// file.Close() // undefined: file
return nil
}
// Better: see how to actually handle this
func readConfigFixed() error {
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("cannot open config: %w", err)
}
defer file.Close() // Now properly closed
// Use file...
_ = file
return nil
}
func main() {
if err := readConfig(); err != nil {
fmt.Println("Error:", err)
}
}
Hint
When you open a file in the if-init statement, where is that file handle accessible? When does it get closed?Solution
**Bug:** The `file` opened in the init statement is scoped to the if-else block and is never closed. Resource leak! **Root cause:** Init statement scope means `defer file.Close()` cannot be called inside the if block (it would close before leaving the function's scope), and the variable isn't available after the if-else. **Lesson:** For resources that need to be closed, declare them before the if statement, not inside it.Bug 10 🔴 — Condition with Side Effect Called Multiple Times¶
package main
import (
"fmt"
"time"
)
var callCount int
func checkService() bool {
callCount++
fmt.Printf(" [checkService called, count=%d]\n", callCount)
time.Sleep(10 * time.Millisecond) // expensive!
return callCount%2 == 0 // alternates true/false
}
func handleRequest() {
// BUG: checkService is called TWICE
if checkService() {
fmt.Println("Service is up")
} else if !checkService() { // Called again here!
fmt.Println("Service is definitely down")
} else {
fmt.Println("Service state unknown")
}
}
func main() {
handleRequest()
fmt.Printf("Total calls: %d (expected: 1)\n", callCount)
}
Hint
How many times is `checkService()` called in the if-else chain? What if the function has side effects or is expensive?Solution
**Bug:** `checkService()` is called up to twice — once in the `if` condition and potentially once more in the `else if` condition. This: 1. Doubles the latency (10ms × 2) 2. May return different results (state changed between calls) 3. Increments the counter twice **Root cause:** Function calls in conditions are re-evaluated for each branch. If the function has side effects or is expensive, always cache the result. **Lesson:** Never call a function with side effects or significant cost more than once in an if-else chain. Store the result in a variable first.Bug 11 🔴 — Nil Map Access in if Condition¶
package main
import "fmt"
type Router struct {
routes map[string]func()
}
func (r *Router) Handle(path string) {
// BUG: if r.routes is nil, this panics
if handler, ok := r.routes[path]; ok {
handler()
} else {
fmt.Println("404: not found")
}
}
func main() {
r := &Router{} // routes map is nil!
r.Handle("/home") // PANIC: assignment to entry in nil map?
// Actually: reading from nil map is OK in Go, but let's verify...
}
Hint
In Go, reading from a nil map is safe (returns zero value). But is WRITING to a nil map safe?Solution
**Surprising fact:** Reading from a nil map in Go is SAFE — it returns the zero value and `ok=false`. So `r.routes[path]` when `routes` is nil will return `(nil, false)`, NOT panic. However, WRITING to a nil map DOES panic: The actual bug in this code is subtle — `r.routes` being nil might be unexpected behavior for users. Better design: **Lesson:** Reading nil maps is safe in Go. Writing is a panic. Always initialize maps with `make()`.Bug 12 🔴 — Logic Error in Complex Boolean¶
package main
import "fmt"
// Check if user can post: must be verified AND (premium OR admin),
// but NOT banned, and account age > 7 days
func canPost(verified, premium, admin, banned bool, accountAgeDays int) bool {
// BUG: operator precedence and logic error
if verified && premium || admin && !banned && accountAgeDays > 7 {
return true
}
return false
}
func main() {
// Test: banned admin with new account — should be FALSE
fmt.Println(canPost(true, false, true, true, 1)) // Expected: false
// Output: true ← BUG!
// Why? && has higher precedence than ||
// Parsed as: (verified && premium) || (admin && !banned && accountAgeDays > 7)
// For banned=true: (true && false) || (true && false && false) = false || false = false
// Wait, that's false... let me reconsider the bug
// Test: unverified premium user — should be FALSE
fmt.Println(canPost(false, true, false, false, 30))
// Parsed: (false && true) || (false && true && true) = false || false = false
// That's correct...
// The real bug: verified && premium misses accountAgeDays check!
// A verified premium user with account age = 1 day can post!
fmt.Println(canPost(true, true, false, false, 1))
// Expected: FALSE (account too new)
// Got: TRUE (verified && premium is true, skips accountAgeDays check)
}
Hint
The condition `verified && premium` does not check `accountAgeDays > 7`. A premium user with a 1-day-old account can post!Solution
**Bug:** The `accountAgeDays > 7` check is missing from the premium path due to incorrect operator grouping.func canPost(verified, premium, admin, banned bool, accountAgeDays int) bool {
// Must not be banned
if banned {
return false
}
// Account must be old enough
if accountAgeDays <= 7 {
return false
}
// Must be verified
if !verified {
return false
}
// Must have elevated access
if !premium && !admin {
return false
}
return true
}
// Or with explicit parentheses:
func canPostExplicit(verified, premium, admin, banned bool, accountAgeDays int) bool {
return !banned &&
accountAgeDays > 7 &&
verified &&
(premium || admin)
}