Mastering Iteration in Go: A Deep Dive into for Loops
Lukas Schneider
DevOps Engineer · Leapcell

Go's approach to iteration is both elegant and pragmatic. Unlike many other languages that offer while
, do-while
, and various other loop constructs, Go simplifies everything down to a single, highly flexible for
keyword. This seemingly limited choice actually grants immense power and clarity, allowing developers to express diverse looping patterns with ease. In this article, we'll take a deep dive into the various forms of Go's for
loop: the traditional C-style three-component loop and the idiomatic for-range
loop.
The Traditional for
Loop: The Three-Component Form
The most basic and widely recognized form of Go's for
loop is equivalent to the C-style for
loop, consisting of three optional components separated by semicolons: initialization
, condition
, and post-statement
.
The general syntax is:
for initialization; condition; post-statement { // loop body }
Let's break down each component:
-
initialization
: This statement executes once before the loop begins. It's typically used to declare and initialize a loop counter or any variable needed for the loop's execution. Variables declared here are scoped only to thefor
loop. -
condition
: This boolean expression is evaluated before each iteration. If it evaluates totrue
, the loop body executes. If it evaluates tofalse
, the loop terminates. If omitted, the condition defaults totrue
, creating an infinite loop (which can be broken usingbreak
). -
post-statement
: This statement executes after each iteration of the loop body. It's commonly used to update the loop counter (e.g.,i++
).
Here's a classic example of iterating from 0 to 4:
package main import "fmt" func main() { fmt.Println("--- Traditional for loop ---") for i := 0; i < 5; i++ { fmt.Printf("Iteration %d\n", i) } }
Output:
--- Traditional for loop ---
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Flexibility: Omitting Components
Go's for
loop design is incredibly flexible because all three components are optional.
1. Omitting initialization
and post-statement
(The "While" Loop)
If you omit the initialization
and post-statement
parts, the for
loop behaves like a while
loop from other languages. You still need the semicolons if the condition
is present.
package main import "fmt" func main() { sum := 1 fmt.Println("\n--- For loop as 'while' loop ---") for sum < 1000 { // No init or post-statement sum += sum fmt.Printf("Current sum: %d\n", sum) } fmt.Printf("Final sum after 'while' loop: %d\n", sum) }
Output:
--- For loop as 'while' loop ---
Current sum: 2
Current sum: 4
Current sum: 8
Current sum: 16
Current sum: 32
Current sum: 64
Current sum: 128
Current sum: 256
Current sum: 512
Current sum: 1024
Final sum after 'while' loop: 1024
Notice that if initialization
is omitted, sum
must be declared outside the loop to be accessible within it and after its termination.
2. Omitting All Components (The Infinite Loop)
If you omit all three components, you create an infinite loop. This pattern is useful for servers, background processes, or situations where you rely on break
statements to exit the loop.
package main import ( "fmt" "time" ) func main() { fmt.Println("\n--- Infinite for loop ---") counter := 0 for { // Forever loop fmt.Printf("Tick %d...\n", counter) counter++ time.Sleep(500 * time.Millisecond) // Simulate some work if counter >= 3 { fmt.Println("Breaking infinite loop.") break // Exit the loop } } fmt.Println("Infinite loop terminated.") }
Output:
--- Infinite for loop ---
Tick 0...
Tick 1...
Tick 2...
Breaking infinite loop.
Infinite loop terminated.
The for-range
Loop: Iterating Over Collections
While the three-component for
loop is excellent for counter-based iterations, Go provides a more convenient and idiomatic way to iterate over collections like arrays, slices, strings, maps, and channels: the for-range
loop.
The for-range
loop simplifies iteration by providing access to both the index/key and the value of elements in a collection, eliminating the need for manual index management.
The general syntax is:
for index, value := range collection { // loop body }
Or, if you only need the value:
for _, value := range collection { // loop body }
And if you only need the index/key:
for index := range collection { // value is discarded implicitly // loop body }
Let's explore its use with different data types.
1. Iterating Over Slices and Arrays
When iterating over a slice or array, range
returns two values for each element: the index and a copy of the element's value.
package main import "fmt" func main() { numbers := []int{10, 20, 30, 40, 50} fmt.Println("\n--- For-range over a slice ---") for i, num := range numbers { fmt.Printf("Index: %d, Value: %d\n", i, num) } // Only interested in values fmt.Println("\n--- For-range over a slice (values only) ---") total := 0 for _, num := range numbers { // Use blank identifier '_' to ignore index total += num } fmt.Printf("Total sum of numbers: %d\n", total) // Only interested in indices fmt.Println("\n--- For-range over a slice (indices only) ---") for i := range numbers { // Value is not returned or used fmt.Printf("Processing element at index: %d\n", i) } }
Output:
--- For-range over a slice ---
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
Index: 3, Value: 40
Index: 4, Value: 50
--- For-range over a slice (values only) ---
Total sum of numbers: 150
--- For-range over a slice (indices only) ---
Processing element at index: 0
Processing element at index: 1
Processing element at index: 2
Processing element at index: 3
Processing element at index: 4
2. Iterating Over Strings
When iterating over a string, range
provides the byte index of the starting character and the Unicode rune (character) itself. This is crucial for correctly handling multi-byte Unicode characters.
package main import "fmt" func main() { sentence := "Hello, 世界" // "世界" are multi-byte Unicode characters fmt.Println("\n--- For-range over a string ---") for i, r := range sentence { fmt.Printf("Byte Index: %2d, Rune: '%c' (Unicode: %U)\n", i, r, r) } }
Output:
--- For-range over a string ---
Byte Index: 0, Rune: 'H' (Unicode: U+0048)
Byte Index: 1, Rune: 'e' (Unicode: U+0065)
Byte Index: 2, Rune: 'l' (Unicode: U+006C)
Byte Index: 3, Rune: 'l' (Unicode: U+006C)
Byte Index: 4, Rune: 'o' (Unicode: U+006F)
Byte Index: 5, Rune: ',' (Unicode: U+002C)
Byte Index: 6, Rune: ' ' (Unicode: U+0020)
Byte Index: 7, Rune: '世' (Unicode: U+4E16)
Byte Index: 10, Rune: '界' (Unicode: U+754C)
Notice the byte indices skip from 6 to 7, and then from 7 to 10 for the multi-byte characters 世
and 界
. range
correctly iterates over runes, not individual bytes.
3. Iterating Over Maps
When iterating over a map, range
returns key-value pairs. The order of iteration over map elements is not guaranteed and can vary.
package main import "fmt" func main() { scores := map[string]int{ "Alice": 95, "Bob": 88, "Charlie": 92, } fmt.Println("\n--- For-range over a map ---") for name, score := range scores { fmt.Printf("%s scored %d\n", name, score) } // Only interested in keys fmt.Println("\n--- For-range over a map (keys only) ---") fmt.Println("Students in the class:") for name := range scores { // Score is not returned or used fmt.Printf("- %s\n", name) } }
Output (order may vary):
--- For-range over a map ---
Alice scored 95
Bob scored 88
Charlie scored 92
--- For-range over a map (keys only) ---
Students in the class:
- Alice
- Bob
- Charlie
4. Iterating Over Channels
When iterating over a channel, range
reads values from the channel until it is closed.
package main import ( "fmt" "time" ) func main() { fmt.Println("\n--- For-range over a channel ---") ch := make(chan int) // Goroutine to send numbers to the channel go func() { for i := 0; i < 3; i++ { ch <- i * 10 time.Sleep(100 * time.Millisecond) } close(ch) // Important: close the channel when done sending }() // Main goroutine to receive numbers from the channel for num := range ch { fmt.Printf("Received: %d\n", num) } fmt.Println("Channel closed, iteration complete.") }
Output:
--- For-range over a channel ---
Received: 0
Received: 10
Received: 20
Channel closed, iteration complete.
The for-range
loop continues to receive values until the ch
channel is explicitly close
d by the sender. If the channel is not closed, the loop will block indefinitely, leading to a deadlock if no more values are sent.
When to Use Which?
-
Three-component
for
loop:- When you need precise control over the loop counter (e.g., iterating a specific number of times, stepping by more than one, iterating backwards).
- When the iteration logic doesn't directly map to a collection.
- When implementing a "while" or "infinite" loop pattern.
-
for-range
loop:- The preferred way to iterate over elements of slices, arrays, strings, maps, and channels.
- When you need both the index/key and the value (or either one).
- When you want clear, concise, and idiomatic code for collection traversal.
- It automatically handles the length of the collection and the complexities of Unicode in strings, reducing common off-by-one errors.
Conclusion
Go's unified for
loop is a testament to its design philosophy of simplicity and power. By consolidating all iteration constructs into a single keyword, Go streamlines the learning curve and fosters consistency. The traditional three-component for
loop provides granular control for sequential and conditional iterations, while the for-range
loop offers an elegant and safer way to traverse collections. Mastering both forms is fundamental to writing effective and idiomatic Go programs. Embracing these powerful iteration tools will enable you to efficiently process data and build robust applications.