Skip to main content
  1. Tutorials/

Foreach Loop in Go: Range, Iterate, and Modern Patterns

··6 mins

Go doesn’t have a foreach keyword like other languages, but it has something even better - the for...range loop. This powerful construct lets you iterate over slices, arrays, maps, strings, channels, and even integers (since Go 1.22). It’s cleaner, safer, and more idiomatic than traditional C-style loops.

In this guide, you’ll learn all the ways to iterate in Go, including the latest features from Go 1.22+ that make loops safer and more expressive. πŸ”„


Basic Range Syntax #

Using the range keyword, you can create the range form of the for loop that is very useful when iterating over a slice or map. This kind of loop has the form of:

for <index>, <value> := range <array/slice> {
    ...
}

where:

  • <index> is a numeric ordinal number that returns 0 for the first element in the array, 1 for the second, and so on
  • <value> is a copy of a slice/array element at that <index>

For maps, the for range loop has <key> instead of <index>:

for <key>, <value> := range <map> {
    ...
}

where:

  • <key> is the key of a given map entry
  • <value> is a copy of a map element at that <key>

Basic Examples #

Iterating Over Slices #

In the following example, we iterate through a slice to print each element. We don’t need the index, so we ignore it using the blank identifier (underscore):

fruits := []string{"apple", "strawberry", "raspberry"}

for _, fruit := range fruits {
    fmt.Printf("Fruit: %s\n", fruit)
}

Output:

Fruit: apple
Fruit: strawberry
Fruit: raspberry

Iterating Over Maps #

When iterating over maps, you get both the key and value:

fruitColors := map[string]string{
    "apple":      "green",
    "strawberry": "red",
    "raspberry":  "pink",
}

for fruit, color := range fruitColors {
    fmt.Printf("%s color is %s\n", fruit, color)
}

Output:

apple color is green
strawberry color is red
raspberry color is pink

Modern Go Features (Go 1.22+) #

Go 1.22 introduced game-changing improvements to for loops that make your code cleaner and safer.

Range Over Integers #

Since Go 1.22, you can range directly over integers - no more for i := 0; i < n; i++!

// Old way (still works)
for i := 0; i < 5; i++ {
    fmt.Printf("Iteration %d\n", i)
}

// New way (Go 1.22+) ✨
for i := range 5 {
    fmt.Printf("Iteration %d\n", i)
}

Both output:

Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4

This is especially useful for simple counting loops and makes the code more readable.

Loop Variable Scoping (Important!) #

Major improvement in Go 1.22: Loop variables are now per-iteration scoped. This fixes a common bug that plagued Go developers for years.

// Before Go 1.22 - BUG! All goroutines would print the same value
values := []int{1, 2, 3, 4, 5}
for _, v := range values {
    go func() {
        fmt.Println(v)  // Would often print 5, 5, 5, 5, 5
    }()
}

// Go 1.22+ - FIXED! Each goroutine gets its own copy
for _, v := range values {
    go func() {
        fmt.Println(v)  // Correctly prints 1, 2, 3, 4, 5
    }()
}

Note: If your go.mod has go 1.22 or later, this fix is automatically enabled. No code changes needed! πŸŽ‰

Range Over Functions (Go 1.23+) #

Go 1.23 introduced the ability to range over custom iterator functions:

// Custom iterator that yields even numbers
func evenNumbers(max int) func(yield func(int) bool) {
    return func(yield func(int) bool) {
        for i := 0; i < max; i += 2 {
            if !yield(i) {
                return
            }
        }
    }
}

// Use it like a regular range loop
for num := range evenNumbers(10) {
    fmt.Println(num)  // Prints: 0, 2, 4, 6, 8
}

Common Mistakes #

1. Assuming Map Iteration Order #

Problem: Map iteration order is randomized for security reasons.

m := map[string]int{"a": 1, "b": 2, "c": 3}

// ❌ Wrong - order is not guaranteed
for k, v := range m {
    fmt.Println(k, v)  // Order may vary between runs
}

// βœ… Correct - sort keys if you need consistent order
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println(k, m[k])  // Guaranteed alphabetical order
}

2. Modifying Slice During Iteration #

numbers := []int{1, 2, 3, 4, 5}

// ❌ Dangerous - can skip elements or panic
for i, num := range numbers {
    if num%2 == 0 {
        numbers = append(numbers[:i], numbers[i+1:]...)  // Modifying while iterating
    }
}

// βœ… Correct - iterate backwards or use a separate result slice
result := make([]int, 0, len(numbers))
for _, num := range numbers {
    if num%2 != 0 {
        result = append(result, num)
    }
}

3. Forgetting Range Returns a Copy #

type Person struct {
    Name string
    Age  int
}

people := []Person{{"Alice", 25}, {"Bob", 30}}

// ❌ Wrong - modifying a copy, original unchanged
for _, p := range people {
    p.Age++  // This doesn't modify the original slice
}

// βœ… Correct - use index to modify original
for i := range people {
    people[i].Age++
}

// βœ… Also correct - use pointer slice
peoplePtr := []*Person{{"Alice", 25}, {"Bob", 30}}
for _, p := range peoplePtr {
    p.Age++  // This modifies the original because p is a pointer
}

Performance Considerations #

Use Index-Only Loops for Large Structs #

When iterating over slices of large structs, use index-only iteration to avoid copying:

type LargeStruct struct {
    Data [1024]byte
}

items := make([]LargeStruct, 1000)

// ❌ Slower - copies each struct
for _, item := range items {
    // item is a copy of items[i]
    processStruct(item)
}

// βœ… Faster - no copies
for i := range items {
    // items[i] is accessed directly
    processStruct(items[i])
}

Range Over Strings is UTF-8 Aware #

text := "Hello δΈ–η•Œ"

// Iterates over runes (UTF-8 code points), not bytes
for i, r := range text {
    fmt.Printf("Position %d: %c (Unicode: %U)\n", i, r, r)
}

Output:

Position 0: H (Unicode: U+0048)
Position 1: e (Unicode: U+0065)
Position 2: l (Unicode: U+006C)
Position 3: l (Unicode: U+006C)
Position 4: o (Unicode: U+006F)
Position 5:   (Unicode: U+0020)
Position 6: δΈ– (Unicode: U+4E16)
Position 9: η•Œ (Unicode: U+754C)

Notice position 6 jumps to 9 because the Chinese characters are multi-byte.


Master other loop patterns and iteration techniques:


Tested with Go 1.25+ | Last verified: December 2025

Happy iterating! πŸš€