Skip to content

if Statement — Practice Tasks

Task 1: Basic Conditionals (Beginner)

Goal: Practice basic if, else if, else syntax.

Requirements: - Write a grade(score int) string function - Returns: "A" (90+), "B" (80-89), "C" (70-79), "D" (60-69), "F" (below 60) - Handle negative scores and scores > 100 with "invalid" - Print results for: -1, 45, 60, 70, 80, 90, 100, 101

Starter Code:

package main

import "fmt"

func grade(score int) string {
    // TODO: implement using if-else if-else
    return ""
}

func main() {
    scores := []int{-1, 45, 60, 70, 80, 90, 100, 101}
    for _, s := range scores {
        fmt.Printf("score=%-4d grade=%s\n", s, grade(s))
    }
}

Expected Output:

score=-1   grade=invalid
score=45   grade=F
score=60   grade=D
score=70   grade=C
score=80   grade=B
score=90   grade=A
score=100  grade=A
score=101  grade=invalid

Evaluation Checklist: - [ ] No parentheses around condition - [ ] Braces always used - [ ] All 8 test cases produce correct output - [ ] "invalid" case handles both < 0 and > 100


Task 2: Error Checking with if (Beginner)

Goal: Practice the if err != nil pattern.

Requirements: - Function parseAndDouble(s string) (int, error) that: - Parses s as integer using strconv.Atoi - If parsing fails, returns 0 and the error - If value is negative, returns 0 and errors.New("value must be non-negative") - Otherwise returns value * 2 and nil - Test with: "42", "-5", "abc", "0", "100"

Starter Code:

package main

import (
    "errors"
    "fmt"
    "strconv"
)

func parseAndDouble(s string) (int, error) {
    // TODO: use if err := strconv.Atoi(s); err != nil { }
    // TODO: check if negative
    // TODO: return doubled value
    return 0, nil
}

func main() {
    inputs := []string{"42", "-5", "abc", "0", "100"}
    for _, input := range inputs {
        result, err := parseAndDouble(input)
        if err != nil {
            fmt.Printf("input=%-5q error=%v\n", input, err)
        } else {
            fmt.Printf("input=%-5q result=%d\n", input, result)
        }
    }
}

Expected Output:

input="42"  result=84
input="-5"  error=value must be non-negative
input="abc" error=strconv.Atoi: parsing "abc": invalid syntax
input="0"   result=0
input="100" result=200

Evaluation Checklist: - [ ] Uses if err := strconv.Atoi(s); err != nil init-statement pattern - [ ] Negative value check with custom error - [ ] All 5 test cases correct - [ ] Error checking with if err != nil in main


Task 3: Guard Clauses (Beginner-Intermediate)

Goal: Refactor nested if to guard clauses.

Requirements: - Start with the nested version (provided in starter) - Refactor processPayment to use guard clauses - Add proper error messages for each failure case - Keep the same behavior, just flatten the nesting

Starter Code:

package main

import (
    "errors"
    "fmt"
)

type Payment struct {
    Amount    float64
    Currency  string
    CardToken string
    UserID    int
}

// NESTED version (refactor this):
func processPaymentNested(p Payment) error {
    if p.Amount > 0 {
        if p.Currency != "" {
            if p.CardToken != "" {
                if p.UserID > 0 {
                    fmt.Printf("Processing $%.2f %s for user %d\n",
                        p.Amount, p.Currency, p.UserID)
                    return nil
                } else {
                    return errors.New("invalid user ID")
                }
            } else {
                return errors.New("card token required")
            }
        } else {
            return errors.New("currency required")
        }
    } else {
        return errors.New("amount must be positive")
    }
}

// TODO: implement using guard clauses
func processPayment(p Payment) error {
    return nil
}

func main() {
    payments := []Payment{
        {Amount: 99.99, Currency: "USD", CardToken: "tok_123", UserID: 42},
        {Amount: -5, Currency: "USD", CardToken: "tok_123", UserID: 42},
        {Amount: 10, Currency: "", CardToken: "tok_123", UserID: 42},
        {Amount: 10, Currency: "EUR", CardToken: "", UserID: 42},
        {Amount: 10, Currency: "EUR", CardToken: "tok_456", UserID: 0},
    }

    for i, p := range payments {
        fmt.Printf("Payment %d: ", i+1)
        if err := processPayment(p); err != nil {
            fmt.Printf("FAILED — %v\n", err)
        } else {
            fmt.Println("OK")
        }
    }
}

Evaluation Checklist: - [ ] Guard clause version has at most 1 level of nesting (no nesting for success path) - [ ] Same 5 test cases pass - [ ] All error messages match the nested version - [ ] Flat, readable code


Task 4: if Init Statement Scope (Intermediate)

Goal: Understand and use variable scoping in if init statements.

Requirements: - Write lookupConfig(key string) (string, bool) that reads from a hardcoded map - Use if val, ok := lookupConfig(key); ok { } pattern - Write loadAllConfigs(keys []string) (map[string]string, []string) that: - Returns found configs as map - Returns list of missing keys - Demonstrate that val and ok are not accessible outside the if

Starter Code:

package main

import "fmt"

var config = map[string]string{
    "host":     "localhost",
    "port":     "8080",
    "database": "mydb",
}

func lookupConfig(key string) (string, bool) {
    // TODO: look up key in config map
    return "", false
}

func loadAllConfigs(keys []string) (found map[string]string, missing []string) {
    found = make(map[string]string)
    // TODO: for each key, use if val, ok := lookupConfig(key); ok { }
    // if found: add to found map
    // if missing: add to missing list
    return
}

func main() {
    keys := []string{"host", "port", "database", "timeout", "retry_count"}

    found, missing := loadAllConfigs(keys)

    fmt.Println("Found configs:")
    for k, v := range found {
        fmt.Printf("  %s = %s\n", k, v)
    }

    fmt.Println("Missing keys:")
    for _, k := range missing {
        fmt.Printf("  %s\n", k)
    }
}

Expected Output:

Found configs:
  host = localhost
  port = 8080
  database = mydb
Missing keys:
  timeout
  retry_count

Evaluation Checklist: - [ ] Uses if val, ok := lookupConfig(key); ok pattern - [ ] val and ok not used outside the if block - [ ] Found map has 3 entries - [ ] Missing list has 2 entries


Task 5: Boolean Short-Circuit Safety (Intermediate)

Goal: Use short-circuit evaluation for safe nil dereference.

Requirements: - Create a Node struct with Value int, Next *Node - Write safeNext(n *Node) *Node that returns n.Next or nil if n is nil - Write safeValue(n *Node) (int, bool) that returns n.Value or (0, false) if n is nil - Write findFirst(head *Node, target int) *Node using && short-circuit safely - Build a linked list and test traversal

Starter Code:

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

func safeNext(n *Node) *Node {
    // TODO: use if n != nil to avoid panic
    return nil
}

func safeValue(n *Node) (int, bool) {
    // TODO: return (0, false) if n is nil
    return 0, false
}

func findFirst(head *Node, target int) *Node {
    current := head
    for current != nil {
        // TODO: use short-circuit: current != nil && current.Value == target
        if current.Value == target {
            return current
        }
        current = current.Next
    }
    return nil
}

func main() {
    // Build: 1 → 2 → 3 → 4 → 5 → nil
    head := &Node{1, &Node{2, &Node{3, &Node{4, &Node{5, nil}}}}}

    // Test safeValue
    if v, ok := safeValue(head); ok {
        fmt.Println("Head value:", v) // 1
    }
    if v, ok := safeValue(nil); ok {
        fmt.Println("Should not print:", v)
    } else {
        fmt.Println("Nil node: no value")
    }

    // Test findFirst
    if n := findFirst(head, 3); n != nil {
        fmt.Println("Found 3, next value:", n.Next.Value) // 4
    }
    if n := findFirst(head, 99); n != nil {
        fmt.Println("Found 99") // should not print
    } else {
        fmt.Println("99 not found")
    }
}

Evaluation Checklist: - [ ] safeValue(nil) returns (0, false) — no panic - [ ] safeNext(nil) returns nil — no panic - [ ] Uses if n != nil && ... pattern - [ ] All test cases pass


Task 6: Multi-Layer Validation (Intermediate)

Goal: Build a layered validation system using if guard clauses.

Requirements: - UserRegistration struct: Name, Email, Password, Age int - validateName(name string) error: non-empty, 2-50 chars - validateEmail(email string) error: non-empty, contains @ and . - validatePassword(pass string) error: 8+ chars, must contain at least one digit - validateAge(age int) error: 18-120 - validateRegistration(reg UserRegistration) []error: collects ALL errors (not fail-fast)

Starter Code:

package main

import (
    "fmt"
    "strings"
    "unicode"
)

type UserRegistration struct {
    Name     string
    Email    string
    Password string
    Age      int
}

func validateName(name string) error {
    // TODO: guard clauses for length checks
    return nil
}

func validateEmail(email string) error {
    // TODO: guard clauses for @ and . checks
    return nil
}

func validatePassword(pass string) error {
    // TODO: length check, then digit check
    return nil
}

func validateAge(age int) error {
    // TODO: range check
    return nil
}

func validateRegistration(reg UserRegistration) []error {
    var errs []error
    // TODO: call each validator, collect non-nil errors
    return errs
}

func main() {
    regs := []UserRegistration{
        {"Alice", "alice@example.com", "password1", 25},    // valid
        {"", "bad-email", "short", 15},                      // all invalid
        {"Bob", "bob@test.com", "nodigits!", 30},            // password invalid
        {"X", "valid@email.com", "valid123", 200},           // name + age invalid
    }

    for i, reg := range regs {
        errs := validateRegistration(reg)
        if len(errs) == 0 {
            fmt.Printf("Registration %d: VALID\n", i+1)
        } else {
            fmt.Printf("Registration %d: %d error(s)\n", i+1, len(errs))
            for _, e := range errs {
                fmt.Printf("  - %v\n", e)
            }
        }
    }
}

Evaluation Checklist: - [ ] All validators use guard clauses (not nested if) - [ ] validateRegistration collects ALL errors (not just first) - [ ] Registration 1 is valid - [ ] Registration 2 has 4 errors - [ ] Password validation checks for digit


Task 7: Error Wrapping and if Dispatch (Intermediate-Advanced)

Goal: Practice error wrapping and dispatching errors in if conditions.

Requirements: - Define sentinel errors: ErrNotFound, ErrPermission, ErrTimeout - fetchResource(id string, userRole string) (*Resource, error) that: - Returns ErrNotFound if id doesn't exist - Returns ErrPermission if userRole is not "admin" - Wraps errors: fmt.Errorf("fetchResource: %w", err) - HTTP handler that dispatches error to status code using errors.Is

Starter Code:

package main

import (
    "errors"
    "fmt"
    "net/http"
)

var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("timeout")
)

type Resource struct {
    ID   string
    Data string
}

var db = map[string]*Resource{
    "r1": {ID: "r1", Data: "secret data"},
    "r2": {ID: "r2", Data: "public data"},
}

func fetchResource(id string, userRole string) (*Resource, error) {
    // TODO: check if id exists, return wrapped ErrNotFound if not
    // TODO: check if userRole == "admin", return wrapped ErrPermission if not
    // TODO: return the resource
    return nil, nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    role := r.Header.Get("X-Role")

    resource, err := fetchResource(id, role)
    if err != nil {
        // TODO: use errors.Is to dispatch to correct status code
        // ErrNotFound → 404
        // ErrPermission → 403
        // default → 500
        return
    }

    fmt.Fprintf(w, "Resource: %s", resource.Data)
}

func main() {
    // Test without HTTP server
    tests := []struct{ id, role string }{
        {"r1", "admin"},
        {"r1", "user"},
        {"r99", "admin"},
    }

    for _, t := range tests {
        r, err := fetchResource(t.id, t.role)
        if err != nil {
            switch {
            case errors.Is(err, ErrNotFound):
                fmt.Printf("id=%s role=%s → 404 Not Found\n", t.id, t.role)
            case errors.Is(err, ErrPermission):
                fmt.Printf("id=%s role=%s → 403 Forbidden\n", t.id, t.role)
            default:
                fmt.Printf("id=%s role=%s → 500 Error: %v\n", t.id, t.role, err)
            }
        } else {
            fmt.Printf("id=%s role=%s → 200 Data: %s\n", t.id, t.role, r.Data)
        }
    }
}

Expected Output:

id=r1 role=admin → 200 Data: secret data
id=r1 role=user → 403 Forbidden
id=r99 role=admin → 404 Not Found

Evaluation Checklist: - [ ] errors.Is correctly detects wrapped errors - [ ] All 3 test cases produce correct output - [ ] fmt.Errorf("...: %w", err) used for wrapping


Task 8: if with Concurrency (Advanced)

Goal: Practice thread-safe if checks with atomic operations.

Requirements: - OnceValue[T] struct: computes a value only once, returns cached value thereafter - Uses sync.Mutex to prevent race conditions - Get(compute func() T) T method: uses if to check if already computed - Test with multiple goroutines calling Get concurrently

Starter Code:

package main

import (
    "fmt"
    "sync"
    "time"
)

type OnceValue[T any] struct {
    mu       sync.Mutex
    computed bool
    value    T
}

func (o *OnceValue[T]) Get(compute func() T) T {
    o.mu.Lock()
    defer o.mu.Unlock()

    // TODO: if not yet computed, call compute() and store result
    // TODO: return cached value

    return o.value
}

func main() {
    callCount := 0
    var ov OnceValue[string]

    compute := func() string {
        callCount++
        time.Sleep(10 * time.Millisecond) // simulate work
        return fmt.Sprintf("computed-%d", callCount)
    }

    // Call from multiple goroutines
    var wg sync.WaitGroup
    results := make([]string, 5)

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            results[i] = ov.Get(compute)
        }(i)
    }

    wg.Wait()

    // All results should be the same (computed only once)
    for i, r := range results {
        fmt.Printf("goroutine %d: %s\n", i, r)
    }
    fmt.Printf("compute() called: %d time(s)\n", callCount)
    // Expected: 1 time
}

Evaluation Checklist: - [ ] compute() is called exactly once - [ ] All 5 goroutines get the same value - [ ] if !o.computed guards the computation - [ ] No data race (test with: go test -race)


Task 9: Feature Flag System (Advanced)

Goal: Build a feature flag system using if for runtime toggling.

Requirements: - FeatureFlags struct with flags loaded from environment or config - IsEnabled(name string) bool method - Handlers use if flags.IsEnabled("new_checkout") to switch behavior - Support overriding flags at runtime - Test that flags correctly switch behavior

Starter Code:

package main

import (
    "fmt"
    "os"
    "sync"
)

type FeatureFlags struct {
    mu    sync.RWMutex
    flags map[string]bool
}

func NewFeatureFlags() *FeatureFlags {
    flags := map[string]bool{}

    // TODO: load from environment variables
    // e.g., FEATURE_NEW_CHECKOUT=true → flags["new_checkout"] = true
    for _, key := range []string{"NEW_CHECKOUT", "DARK_MODE", "BETA_SEARCH"} {
        envKey := "FEATURE_" + key
        if val := os.Getenv(envKey); val == "true" {
            // TODO: add to flags map (use lowercase snake_case key)
        }
    }

    return &FeatureFlags{flags: flags}
}

func (f *FeatureFlags) IsEnabled(name string) bool {
    f.mu.RLock()
    defer f.mu.RUnlock()
    return f.flags[name]
}

func (f *FeatureFlags) Set(name string, enabled bool) {
    f.mu.Lock()
    defer f.mu.Unlock()
    // TODO: set flag
}

func checkoutPage(flags *FeatureFlags) string {
    if flags.IsEnabled("new_checkout") {
        return "new checkout experience"
    }
    return "legacy checkout"
}

func main() {
    flags := NewFeatureFlags()

    fmt.Println("Default:", checkoutPage(flags))  // legacy checkout

    flags.Set("new_checkout", true)
    fmt.Println("After enable:", checkoutPage(flags))  // new checkout experience

    flags.Set("new_checkout", false)
    fmt.Println("After disable:", checkoutPage(flags))  // legacy checkout
}

Evaluation Checklist: - [ ] IsEnabled returns false for unknown flags (zero value of map) - [ ] Set correctly toggles flags - [ ] checkoutPage switches behavior based on flag - [ ] Thread-safe with RWMutex - [ ] Environment variable loading works


Task 10: Circuit Breaker with if State Machine (Advanced)

Goal: Implement a simple circuit breaker using if for state transitions.

Requirements: - States: "closed" (normal), "open" (failing), "half-open" (testing recovery) - Transitions: closed→open (on threshold failures), open→half-open (after timeout), half-open→closed (on success), half-open→open (on failure) - Allow() bool: returns whether a request should be allowed - RecordSuccess() and RecordFailure() update state - Test the state machine with a sequence of successes and failures

Starter Code:

package main

import (
    "fmt"
    "sync"
    "time"
)

type CircuitBreaker struct {
    mu           sync.Mutex
    state        string // "closed", "open", "half-open"
    failures     int
    lastFailure  time.Time
    threshold    int
    timeout      time.Duration
}

func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        state:     "closed",
        threshold: threshold,
        timeout:   timeout,
    }
}

func (cb *CircuitBreaker) Allow() bool {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    if cb.state == "closed" {
        return true
    }

    if cb.state == "open" {
        // TODO: if timeout has passed, transition to half-open and allow
        // Otherwise, deny
        return false
    }

    // half-open: allow one test request
    return true
}

func (cb *CircuitBreaker) RecordSuccess() {
    cb.mu.Lock()
    defer cb.mu.Unlock()
    // TODO: if half-open, close the circuit (reset failures, state = "closed")
}

func (cb *CircuitBreaker) RecordFailure() {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    cb.failures++
    cb.lastFailure = time.Now()

    // TODO: if failures >= threshold, open the circuit
    // TODO: if half-open, go back to open
}

func main() {
    cb := NewCircuitBreaker(3, 50*time.Millisecond)

    simulate := func(success bool) {
        if cb.Allow() {
            if success {
                cb.RecordSuccess()
                fmt.Printf("  → allowed, success (state: %s)\n", cb.state)
            } else {
                cb.RecordFailure()
                fmt.Printf("  → allowed, failure (state: %s)\n", cb.state)
            }
        } else {
            fmt.Printf("  → BLOCKED (state: %s)\n", cb.state)
        }
    }

    fmt.Println("3 failures → open:")
    simulate(false)
    simulate(false)
    simulate(false)

    fmt.Println("\n2 blocked requests:")
    simulate(false)
    simulate(false)

    fmt.Println("\nWait for timeout, then half-open:")
    time.Sleep(60 * time.Millisecond)
    simulate(true) // should allow and close

    fmt.Println("\nClosed again:")
    simulate(true)
    simulate(true)
}

Evaluation Checklist: - [ ] State transitions implemented correctly (all 4 transitions) - [ ] Requests blocked in "open" state - [ ] Timeout correctly transitions to "half-open" - [ ] Success in "half-open" closes the circuit - [ ] Failure in "half-open" reopens the circuit - [ ] Thread-safe with mutex