if Statement — Find the Bug¶
Difficulty: 🟢 Easy | 🟡 Medium | 🔴 Hard
Bug 1 🟢 The Inverted Error Check¶
package main
import (
"fmt"
"os"
)
func main() {
data, err := os.ReadFile("config.json")
if err == nil {
fmt.Println("Error reading file:", err)
return
}
fmt.Printf("File content (%d bytes): %s\n", len(data), data)
}
What should it do: Read a file, print its contents on success, print error on failure.
Hint
The error check condition is checking the wrong thing. When `err == nil`, there is no error.Solution
**Bug:** The condition is inverted. `err == nil` means there is NO error — so the code currently prints "Error reading file" when there's no error, and tries to use `data` (which may be empty/invalid) when there IS an error. **Fix:** **Key Lesson:** In Go, `err == nil` means "no error occurred" (success). `err != nil` means "an error occurred". Always check `err != nil` to detect errors.Bug 2 🟢 Missing Braces on Single-Statement if¶
package main
import "fmt"
func checkAge(age int) string {
if age >= 18
return "adult"
return "minor"
}
func main() {
fmt.Println(checkAge(20))
fmt.Println(checkAge(15))
}
What should it do: Return "adult" for age >= 18, "minor" otherwise.
Hint
Go requires braces around `if` bodies. What happens when they're missing?Solution
**Bug:** Go requires braces `{}` for all `if` bodies — even single-statement bodies. This is a syntax error that the compiler catches. **Fix:** **Key Lesson:** Unlike C/Java, Go does not allow brace-free single-statement `if` bodies. This design choice prevents bugs like Apple's "goto fail" SSL vulnerability.Bug 3 🟢 Checking == true Instead of Direct Boolean¶
package main
import "fmt"
func isEven(n int) bool {
return n%2 == 0
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
for _, n := range numbers {
if isEven(n) == true {
fmt.Printf("%d is even\n", n)
} else if isEven(n) == false {
fmt.Printf("%d is odd\n", n)
}
}
}
What should it do: Print whether each number is even or odd.
Hint
The code works but violates Go idioms. How should boolean values be used in `if` conditions?Solution
**Bug:** Two problems: 1. `isEven(n) == true` is redundant — `isEven(n)` is already a `bool` 2. `isEven(n)` is called twice — unnecessary double computation **Fix:** **Key Lesson:** Use `if flag` not `if flag == true`, and `if !flag` not `if flag == false`. Never call the same function twice in a condition — store the result.Bug 4 🟢 Variable Not Accessible After if Block¶
package main
import (
"fmt"
"strconv"
)
func main() {
input := "42"
if n, err := strconv.Atoi(input); err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Parsed:", n) // using n outside its scope!
}
Hint
Variables declared in the `if` init statement are scoped to the if-else block. Where is `n` accessible?Solution
**Bug:** `n` is declared in the `if` init statement (`n, err := strconv.Atoi(input)`) and is only accessible within the `if-else` block. Using it after the closing `}` is a compile error: `undefined: n`. **Fix — Option 1: Declare before `if`:** **Fix — Option 2: Use `n` inside the `else` block:** **Key Lesson:** Variables declared in an `if` init statement are scoped to the entire `if-else` chain, not to the code after it. If you need the value outside, declare it before the `if`.Bug 5 🟡 Incorrect nil Check on Interface¶
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string { return e.msg }
func getError(fail bool) error {
if fail {
return &MyError{"something failed"}
}
var err *MyError = nil // typed nil!
return err // returns non-nil interface holding nil *MyError
}
func main() {
err := getError(false)
if err != nil {
fmt.Println("Got error:", err) // prints even though we returned "nil"!
} else {
fmt.Println("No error")
}
}
Hint
In Go, an interface value is `nil` only if both its type and value are nil. What happens when you return a typed nil pointer wrapped in an interface?Solution
**Bug:** The function returns `err` (of type `*MyError`, value nil) as an `error` interface. The interface value holds: `{type: *MyError, value: nil}`. This is a **non-nil interface** even though the underlying pointer is nil. The `if err != nil` check sees a non-nil interface. **Fix — Option 1: Return `nil` directly:** **Fix — Option 2: Return typed value, check typed nil:** **Key Lesson:** Never return a typed nil pointer as an interface. Always return the untyped `nil` directly. This is one of Go's most common subtle bugs.Bug 6 🟡 Float Comparison with ==¶
package main
import "fmt"
func calculateTax(amount float64) float64 {
tax := amount * 0.1
return tax
}
func main() {
price := 19.90
tax := calculateTax(price)
total := price + tax
if total == 21.89 {
fmt.Println("Total is correct: 21.89")
} else {
fmt.Printf("Total is wrong: %.20f\n", total)
}
}
Hint
Floating-point arithmetic is not exact. What is `19.90 * 1.1` in IEEE 754 binary representation?Solution
**Bug:** `total == 21.89` uses exact equality with floating-point numbers. Due to binary floating-point representation, `19.90 * 1.1` is not exactly `21.89` — it's approximately `21.890000000000001...`.price = 19.90 (not exactly representable in binary float)
tax = 19.90 * 0.1 ≈ 1.9899999999999999289...
total = 21.8899999999999... ≠ 21.89
import "math"
epsilon := 0.001
if math.Abs(total-21.89) < epsilon {
fmt.Println("Total is approximately correct")
}
// Or for financial calculations: use integer cents
priceInCents := 1990 // $19.90
taxInCents := priceInCents / 10 // integer division: 199
totalInCents := priceInCents + taxInCents // 2189 = $21.89
if totalInCents == 2189 {
fmt.Printf("Total: $%d.%02d\n", totalInCents/100, totalInCents%100)
}
Bug 7 🟡 else After return (Unnecessary Else)¶
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
} else {
result := a / b
if result > 1000 {
return 0, errors.New("result too large")
} else {
return result, nil
}
}
}
func main() {
if r, err := divide(10, 2); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Result: %.2f\n", r)
}
}
What's wrong? The code works correctly but has structural issues.
Hint
When an `if` block always returns, the `else` is logically equivalent to the code just continuing after the `if`.Solution
**Bug:** Redundant `else` blocks after `return` statements — deep nesting that could be flat. **Fixed (guard clause style):** **Also fix the caller (else after return):** **Key Lesson:** If an `if` block always returns, the subsequent `else` is redundant. Drop it for cleaner, flatter code. Linters (`revive`, `staticcheck`) flag this pattern.Bug 8 🟡 Shadowing Variable in if Init¶
package main
import (
"fmt"
"errors"
)
var ErrDatabase = errors.New("database error")
func queryDB() error {
return ErrDatabase
}
func process() error {
err := queryDB()
if err != nil {
if err := fmt.Errorf("process: %w", err); err != nil {
// The developer intended to check the WRAPPED error,
// but actually checks if Errorf itself failed (it never does)
return err
}
}
return nil
}
func main() {
err := process()
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success")
}
}
Hint
Look carefully at the nested `if err :=`. Does `fmt.Errorf` ever return an error? What does the inner `err` shadow?Solution
**Bug:** The code tries to wrap the error, but the structure is wrong: 1. `fmt.Errorf("process: %w", err)` always succeeds and returns a non-nil error — the outer `err != nil` check is always true when we reach the inner block 2. `if err := fmt.Errorf(...); err != nil` — `fmt.Errorf` never returns nil! The inner `err` is always non-nil, so this is effectively `if true` 3. The inner `err` shadows the outer `err` — confusing **Fix:** **Key Lesson:** Don't use `if` init statement to create wrapped errors — just wrap and return. `fmt.Errorf` always returns a non-nil error value, so `if err := fmt.Errorf(...); err != nil` is always true.Bug 9 🔴 Check-Then-Act Race Condition¶
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.Mutex
data map[string]string
}
func (c *Cache) GetOrCreate(key string, create func() string) string {
c.mu.Lock()
val, ok := c.data[key]
c.mu.Unlock() // unlock to allow concurrent reads
if !ok { // check
newVal := create()
c.mu.Lock()
c.data[key] = newVal // act
c.mu.Unlock()
return newVal
}
return val
}
var cache = &Cache{data: make(map[string]string)}
func main() {
// Simulating concurrent access:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
val := cache.GetOrCreate("key", func() string {
fmt.Printf("creating value (goroutine %d)\n", n)
return fmt.Sprintf("value-%d", n)
})
fmt.Printf("goroutine %d got: %s\n", n, val)
}(i)
}
wg.Wait()
}
Hint
Between the "check" (reading `ok`) and the "act" (writing `c.data[key]`), the lock is released. What can happen?Solution
**Bug:** Classic check-then-act (TOCTOU) race condition: 1. Goroutine A checks `!ok` — key not found 2. Lock released 3. Goroutine B also checks `!ok` — also not found (before A writes) 4. Both goroutines call `create()` and write the key 5. `create()` runs multiple times — data is overwritten, last writer wins The `if !ok` check and the subsequent write are not atomic. **Fix — Option 1: Keep lock held for both check and act:** **Fix — Option 2: Double-checked locking with sync.Map:** **Note:** In Fix 2, `create()` may still be called multiple times, but only one result is stored. Use LoadOrStore carefully when `create()` has side effects. **Key Lesson:** Any "check then act" sequence must be protected by holding a lock for the entire sequence. Releasing and re-acquiring the lock creates a race window.Bug 10 🔴 Wrong Error Type Check with Interface¶
package main
import (
"fmt"
"net"
)
func isTimeout(err error) bool {
if netErr, ok := err.(net.Error); ok {
return netErr.Timeout()
}
return false
}
func main() {
// Simulate a wrapped timeout error
originalErr := &net.OpError{
Op: "dial",
Net: "tcp",
Err: &timeoutError{},
}
wrappedErr := fmt.Errorf("connection failed: %w", originalErr)
fmt.Println("isTimeout (direct):", isTimeout(originalErr)) // true
fmt.Println("isTimeout (wrapped):", isTimeout(wrappedErr)) // false! BUG
}
type timeoutError struct{}
func (e *timeoutError) Error() string { return "timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }
Hint
Type assertion `err.(net.Error)` only checks the top-level error type. What happens when the error is wrapped?Solution
**Bug:** `err.(net.Error)` is a type assertion — it only checks if the **top-level** error implements `net.Error`. When `err` is a `*fmt.wrapError` (from `fmt.Errorf(...%w...)`), the type assertion fails even if the wrapped error implements `net.Error`. **Fix — Use `errors.As` which unwraps:**func isTimeout(err error) bool {
var netErr net.Error
if errors.As(err, &netErr) { // unwraps error chain
return netErr.Timeout()
}
return false
}
// Or check the Timeout() method directly:
func isTimeout(err error) bool {
type timeouter interface{ Timeout() bool }
var te timeouter
if errors.As(err, &te) {
return te.Timeout()
}
return false
}
Bug 11 🔴 Condition Always True Due to Unsigned Integer¶
package main
import "fmt"
func processItems(count uint) {
items := make([]string, count)
for i := uint(0); i < count; i++ {
items[i] = fmt.Sprintf("item-%d", i)
}
// Remove last N items
removeCount := uint(5)
if count-removeCount > 0 { // BUG: always true when count < removeCount!
items = items[:count-removeCount]
fmt.Printf("Kept %d items\n", len(items))
} else {
fmt.Println("Removing all items")
items = items[:0]
}
}
func main() {
processItems(3) // count=3, removeCount=5 → should remove all
}
Hint
What happens when you subtract from an unsigned integer when the result would be negative?Solution
**Bug:** `count-removeCount` with `count=3` and `removeCount=5` causes **unsigned integer underflow**. Unsigned integers wrap around: `uint(3) - uint(5) = uint(^uint(0) - 1)` (a very large number). So `count-removeCount > 0` is always true when underflow occurs.uint(3) - uint(5) = 0xFFFFFFFFFFFFFFFE (not -2!)
0xFFFFFFFFFFFFFFFE > 0 → true → always takes the wrong branch
Bug 12 🔴 Panic Instead of Error in Library Function¶
package main
import (
"encoding/json"
"fmt"
)
type Parser struct {
data map[string]interface{}
}
func (p *Parser) Parse(jsonStr string) {
// If unmarshaling fails, the entire program panics
if err := json.Unmarshal([]byte(jsonStr), &p.data); err != nil {
panic(fmt.Sprintf("invalid JSON: %v", err))
}
}
func (p *Parser) GetString(key string) string {
val, ok := p.data[key]
if !ok {
panic(fmt.Sprintf("key not found: %s", key))
}
return val.(string) // also panics if not string!
}
func main() {
p := &Parser{}
p.Parse(`{"name":"Alice","age":30}`)
name := p.GetString("name")
fmt.Println("Name:", name)
// This will panic — should return error instead
p.Parse("not valid json")
}
Hint
When is it appropriate to use `panic` vs returning an error? Library code should handle invalid input gracefully.Solution
**Bug:** Library functions should return errors, not panic. Panics propagate up the call stack and crash the program unless explicitly recovered. This makes the library unusable in a server context (where panics must be recovered in a middleware, losing context). **Fix:**type Parser struct {
data map[string]interface{}
}
func (p *Parser) Parse(jsonStr string) error {
if err := json.Unmarshal([]byte(jsonStr), &p.data); err != nil {
return fmt.Errorf("parse JSON: %w", err)
}
return nil
}
func (p *Parser) GetString(key string) (string, error) {
val, ok := p.data[key]
if !ok {
return "", fmt.Errorf("key %q not found", key)
}
str, ok := val.(string)
if !ok {
return "", fmt.Errorf("key %q is not a string (got %T)", key, val)
}
return str, nil
}
func main() {
p := &Parser{}
if err := p.Parse(`{"name":"Alice","age":30}`); err != nil {
fmt.Println("Error:", err)
return
}
if name, err := p.GetString("name"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Name:", name)
}
// Graceful error handling:
if err := p.Parse("not valid json"); err != nil {
fmt.Println("Parse error:", err) // handled, program continues
}
}
Bug 13 🔴 Lost Error from Deferred Function¶
package main
import (
"fmt"
"os"
)
func writeToFile(filename, content string) error {
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer f.Close() // BUG: ignores Close error!
if _, err := f.Write([]byte(content)); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil // caller thinks everything succeeded, but Close may have failed!
}
func main() {
if err := writeToFile("/tmp/test.txt", "hello"); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success")
}
}
Hint
`f.Close()` can return an error (e.g., if the write buffer couldn't be flushed). How do you capture a deferred function's error return?Solution
**Bug:** `defer f.Close()` discards the error returned by `Close()`. For file writes, `Close()` can fail if the OS couldn't flush the write buffer to disk. The caller sees `nil` (success) but the data may not be persisted. **Fix — named return value captures defer error:**func writeToFile(filename, content string) (err error) {
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close file: %w", cerr)
}
}()
if _, err = f.Write([]byte(content)); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}