BLOG POSTS
How to Construct For Loops in Go

How to Construct For Loops in Go

Go’s for loop is the only looping construct in the language, but don’t let that fool you into thinking it’s limiting. This single construct is incredibly versatile and can handle everything from basic iteration to complex control flow patterns. Understanding for loops is essential for Go developers because they’re used everywhere – from processing slices and maps to implementing servers and handling concurrent operations. In this post, you’ll learn the various forms of for loops in Go, when to use each one, common gotchas that trip up developers, and performance considerations that can make or break your applications.

How Go For Loops Work

Unlike many languages that have multiple loop types (while, do-while, foreach), Go simplifies things with a single for statement that can take several forms. The basic syntax follows this pattern:

for initialization; condition; post {
    // loop body
}

But Go’s for loop is more flexible than this basic form suggests. Here are the main variations:

  • Classic three-part loop: Similar to C-style for loops
  • Condition-only loop: Acts like a while loop
  • Infinite loop: Runs until explicitly broken
  • Range loop: Iterates over collections, channels, or strings

The compiler handles these variations efficiently, often optimizing simple loops into more performant assembly code. Range loops, in particular, receive special treatment and can be significantly faster than manual indexing for certain operations.

Step-by-Step Implementation Guide

Basic Three-Part For Loop

This is your bread-and-butter loop for numeric iteration:

package main

import "fmt"

func main() {
    // Basic counting loop
    for i := 0; i < 10; i++ {
        fmt.Printf("Iteration %d\n", i)
    }
    
    // You can modify any part
    for i := 10; i > 0; i -= 2 {
        fmt.Printf("Countdown: %d\n", i)
    }
    
    // Multiple variables (less common but useful)
    for i, j := 0, 100; i < j; i, j = i+1, j-1 {
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

Condition-Only Loops

When you need while-loop behavior:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())
    
    // Acts like a while loop
    attempts := 0
    for attempts < 5 {
        if rand.Intn(10) > 7 {
            fmt.Printf("Success on attempt %d!\n", attempts+1)
            break
        }
        attempts++
        fmt.Printf("Attempt %d failed, trying again...\n", attempts)
    }
}

Infinite Loops with Break Conditions

Perfect for servers, event loops, or when you need complex exit conditions:

package main

import (
    "fmt"
    "time"
)

func main() {
    counter := 0
    
    for {
        counter++
        fmt.Printf("Running iteration %d\n", counter)
        
        // Multiple exit conditions
        if counter > 10 {
            fmt.Println("Max iterations reached")
            break
        }
        
        if counter%3 == 0 {
            fmt.Println("Skipping some work...")
            continue
        }
        
        // Simulate some work
        time.Sleep(100 * time.Millisecond)
    }
}

Range Loops - The Go Way

Range loops are idiomatic Go and handle most collection iteration needs:

package main

import "fmt"

func main() {
    // Slice iteration
    fruits := []string{"apple", "banana", "cherry", "date"}
    
    // Both index and value
    for i, fruit := range fruits {
        fmt.Printf("Index %d: %s\n", i, fruit)
    }
    
    // Value only (index ignored with _)
    for _, fruit := range fruits {
        fmt.Printf("Fruit: %s\n", fruit)
    }
    
    // Index only
    for i := range fruits {
        fmt.Printf("Index: %d\n", i)
    }
    
    // Map iteration
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
    }
    
    for name, score := range scores {
        fmt.Printf("%s scored %d\n", name, score)
    }
    
    // String iteration (runes, not bytes!)
    text := "Hello, 世界"
    for i, char := range text {
        fmt.Printf("Position %d: %c (Unicode: %U)\n", i, char, char)
    }
}

Real-World Examples and Use Cases

Processing HTTP Server Requests

Here's a realistic example of using for loops in a web server context:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)

type LogEntry struct {
    Timestamp time.Time `json:"timestamp"`
    Method    string    `json:"method"`
    Path      string    `json:"path"`
    Status    int       `json:"status"`
}

func processLogs(logs []LogEntry) map[string]int {
    statusCounts := make(map[string]int)
    
    // Range loop for processing slice
    for _, entry := range logs {
        statusGroup := fmt.Sprintf("%dxx", entry.Status/100)
        statusCounts[statusGroup]++
    }
    
    return statusCounts
}

func serverHealthCheck() {
    endpoints := []string{
        "http://api.example.com/health",
        "http://db.example.com/ping", 
        "http://cache.example.com/status",
    }
    
    // Check each endpoint
    for i, endpoint := range endpoints {
        // Infinite loop with timeout
        attempts := 0
        for {
            resp, err := http.Get(endpoint)
            if err == nil && resp.StatusCode == 200 {
                fmt.Printf("✓ Endpoint %d (%s) is healthy\n", i+1, endpoint)
                resp.Body.Close()
                break
            }
            
            attempts++
            if attempts >= 3 {
                fmt.Printf("✗ Endpoint %d (%s) failed after 3 attempts\n", i+1, endpoint)
                break
            }
            
            fmt.Printf("Retrying endpoint %d, attempt %d...\n", i+1, attempts+1)
            time.Sleep(time.Duration(attempts) * time.Second)
        }
    }
}

Concurrent Processing with Goroutines

For loops are essential when working with channels and goroutines:

package main

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

func worker(id int, jobs <-chan int, results chan<- int) {
    // Infinite loop reading from channel
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(time.Millisecond * 100) // Simulate work
        results <- job * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    // Start 3 worker goroutines
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    
    // Send 9 jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)
    
    // Collect results
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        for a := 1; a <= 9; a++ {
            result := <-results
            fmt.Printf("Result: %d\n", result)
        }
    }()
    
    wg.Wait()
}

Performance Comparisons and Benchmarks

Different loop patterns have varying performance characteristics. Here's a comparison of common iteration approaches:

Loop Type Use Case Performance Memory Usage Best For
Classic for loop Numeric iteration Fastest Minimal Index-based operations
Range over slice Element access Very fast Minimal Processing slice elements
Range over map Key-value pairs Fast Minimal Map processing
Range over string Unicode handling Moderate Low Text processing
Range over channel Concurrent processing Variable Buffer dependent Pipeline processing

Here's a benchmark example you can run to see the differences:

package main

import (
    "fmt"
    "testing"
    "time"
)

func BenchmarkClassicLoop(b *testing.B) {
    slice := make([]int, 1000000)
    for i := range slice {
        slice[i] = i
    }
    
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        sum := 0
        for i := 0; i < len(slice); i++ {
            sum += slice[i]
        }
    }
}

func BenchmarkRangeLoop(b *testing.B) {
    slice := make([]int, 1000000)
    for i := range slice {
        slice[i] = i
    }
    
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        sum := 0
        for _, v := range slice {
            sum += v
        }
    }
}

// Run with: go test -bench=.

Common Pitfalls and Best Practices

The Classic Range Variable Trap

This is probably the most common mistake Go developers make with for loops:

package main

import (
    "fmt"
    "time"
)

func main() {
    // WRONG - captures loop variable by reference
    items := []string{"apple", "banana", "cherry"}
    
    fmt.Println("❌ Wrong way:")
    for _, item := range items {
        go func() {
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Processing: %s\n", item) // Will likely print "cherry" 3 times
        }()
    }
    time.Sleep(500 * time.Millisecond)
    
    fmt.Println("\n✅ Correct way:")
    for _, item := range items {
        go func(item string) { // Pass as parameter
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Processing: %s\n", item)
        }(item)
    }
    time.Sleep(500 * time.Millisecond)
    
    fmt.Println("\n✅ Alternative correct way:")
    for _, item := range items {
        item := item // Create new variable in loop scope
        go func() {
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Processing: %s\n", item)
        }()
    }
    time.Sleep(500 * time.Millisecond)
}

Range Over Maps: Order Isn't Guaranteed

Go deliberately randomizes map iteration order:

package main

import "fmt"

func main() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
        "Dave":  78,
    }
    
    // This order will vary between runs!
    fmt.Println("Map iteration (order not guaranteed):")
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
    
    // If you need consistent order, sort the keys first
    fmt.Println("\nSorted iteration:")
    keys := make([]string, 0, len(scores))
    for name := range scores {
        keys = append(keys, name)
    }
    
    // You'd typically use sort.Strings(keys) here
    for _, name := range keys {
        fmt.Printf("%s: %d\n", name, scores[name])
    }
}

Performance Best Practices

  • Avoid unnecessary allocations: Pre-allocate slices when you know the size
  • Use range for slices: It's optimized and more readable than index loops
  • Be careful with large structs: Range creates copies, consider using pointers
  • Don't modify slices during iteration: Can lead to unexpected behavior
// Good: Pre-allocate when size is known
results := make([]string, 0, len(input))
for _, item := range input {
    results = append(results, process(item))
}

// Better: Direct assignment when possible
results := make([]string, len(input))
for i, item := range input {
    results[i] = process(item)
}

// Careful with large structs
type LargeStruct struct {
    data [1000]int
    // ... other fields
}

items := []LargeStruct{...}

// This copies each struct (expensive!)
for _, item := range items {
    process(item)
}

// This is more efficient
for i := range items {
    process(&items[i])
}

Advanced Patterns and Integration

Loop Labels for Complex Control Flow

Go supports labeled breaks and continues for nested loops:

package main

import "fmt"

func main() {
    // Finding the first pair that matches a condition
outer:
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if i*j > 6 {
                fmt.Printf("Found pair: i=%d, j=%d, product=%d\n", i, j, i*j)
                break outer // Breaks out of both loops
            }
        }
    }
    
    // Using continue with labels
    fmt.Println("\nSkipping certain combinations:")
outer2:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if j == 1 {
                continue outer2 // Continues outer loop
            }
            fmt.Printf("i=%d, j=%d\n", i, j)
        }
    }
}

Integration with Context for Cancellation

Modern Go applications often need cancellable loops:

package main

import (
    "context"
    "fmt"
    "time"
)

func processWithTimeout(ctx context.Context, items []string) error {
    for i, item := range items {
        select {
        case <-ctx.Done():
            return fmt.Errorf("processing cancelled at item %d: %w", i, ctx.Err())
        default:
            // Process the item
            fmt.Printf("Processing item %d: %s\n", i, item)
            time.Sleep(200 * time.Millisecond) // Simulate work
        }
    }
    return nil
}

func main() {
    items := []string{"task1", "task2", "task3", "task4", "task5"}
    
    // Create a context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()
    
    if err := processWithTimeout(ctx, items); err != nil {
        fmt.Printf("Error: %v\n", err)
    }
}

For loops in Go are deceptively simple but incredibly powerful. The key to mastering them is understanding when to use each variant and being aware of the common pitfalls. The Go team has put considerable effort into optimizing loop performance, so following idiomatic patterns usually gives you the best results. For more detailed information about Go's for statement and its optimizations, check out the official Go language specification and the Effective Go guide.

Whether you're processing HTTP requests, managing concurrent operations, or just iterating over data structures, these patterns will serve you well in building robust Go applications. The simplicity of having just one loop construct means less cognitive overhead and more consistent code across your projects.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked