Loops #
Go has only one keyword for looping: for. There’s no while. No do-while. No foreach. This decision isn’t because Go isn’t capable — it’s deliberate, to keep things consistent. When every Go developer uses the same single keyword for every loop pattern, code from one developer is instantly readable by another. And it turns out that one for is flexible enough to express every looping pattern that has ever existed.
The Three Forms of for
#
for in Go can be written in three main forms, each with the right context for its use. A single for keyword in Go can take three main control-flow variations, depending on the looping goal you want. These variations can be understood through the following diagram:
flowchart TD
ForLoop["The for Keyword in Go"] --> Classic["Classic Three-Component\nfor init; cond; post { ... }"]
ForLoop --> WhileStyle["Condition-Only (Equivalent to while)\nfor cond { ... }"]
ForLoop --> Infinite["Infinite Loop\nfor { ... }"]
Classic -->|"Use for"| Counted["Counted Loops (Index/Range)"]
WhileStyle -->|"Use for"| Dynamic["Dynamic Loops (Logical Condition)"]
Infinite -->|"Use for"| Listeners["Background / Worker Daemons"]Form 1: Classic Three-Component #
The form most familiar to developers coming from C, Java, or JavaScript:
// for init; condition; post { }
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// Output: 0 1 2 3 4
The three components are separated by semicolons:
- init (
i := 0) — executed once before the first iteration; variables declared here only live within the loop’s scope - condition (
i < 5) — checked before every iteration; the loop stops when it’sfalse - post (
i++) — executed after every iteration, before the next condition check
Each component is optional — you can drop one or both of them:
// Condition only — equivalent to while
i := 0
for i < 5 {
fmt.Println(i)
i++
}
// Init and post only — the condition is always true → infinite loop
for i := 0; ; i++ {
if i >= 5 {
break
}
fmt.Println(i)
}
// No components at all → infinite loop
for {
// loop forever
}
Form 2: Condition Only (equivalent to while)
#
When there’s only a condition, for behaves like while in other languages:
// Read input until a condition is met
attempts := 0
for attempts < maxRetry {
err := tryConnect()
if err == nil {
break
}
attempts++
time.Sleep(time.Second * time.Duration(attempts))
}
// Process a data stream until it's exhausted
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
processLine(line)
}
Form 3: Infinite Loop #
A bare for is an infinite loop — not a code smell, but a very legitimate Go pattern for servers, daemons, and workers:
// Server loop — runs forever waiting for requests
for {
conn, err := listener.Accept()
if err != nil {
log.Println("accept error:", err)
continue
}
go handleConnection(conn)
}
// Worker loop — processes jobs from a channel
for {
select {
case job := <-jobQueue:
processJob(job)
case <-stopSignal:
return // exit the infinite loop
}
}
for-range — Iterating Collections
#
range is Go’s idiomatic way of iterating collections. It returns two values: the index (or key) and the value. You can ignore either one with the blank identifier _.
Range over Slices and Arrays #
fruits := []string{"apple", "mango", "orange", "durian"}
// Two values: index and value
for i, v := range fruits {
fmt.Printf("[%d] %s\n", i, v)
}
// Value only — index ignored
for _, v := range fruits {
fmt.Println(v)
}
// Index only — value ignored
for i := range fruits {
fmt.Printf("index: %d\n", i)
}
// Modifying elements via the index (range gives a COPY of the value)
numbers := []int{1, 2, 3, 4, 5}
for i := range numbers {
numbers[i] *= 2 // ✓ modify through the index, not through v
}
fmt.Println(numbers) // [2 4 6 8 10]
rangegives a copy of the value, not a reference. Modifyingvinside the loop doesn’t affect the original slice:numbers := []int{1, 2, 3} for _, v := range numbers { v *= 2 // ✗ modifies a local copy, not the original element } fmt.Println(numbers) // [1 2 3] — unchanged! // CORRECT: modify through the index for i := range numbers { numbers[i] *= 2 // ✓ } fmt.Println(numbers) // [2 4 6]
Range over Strings — Per Rune, Not Per Byte #
This is one of the behaviors that most often surprises people. range over a string iterates per rune (Unicode character), not per byte. The index returned is the byte position, not the character position:
s := "Hello, 世界"
// range per RUNE
for i, r := range s {
fmt.Printf("byte index %d: %c (U+%04X)\n", i, r, r)
}
// Output:
// byte index 0: H (U+0048)
// byte index 1: e (U+0065)
// byte index 2: l (U+006C)
// byte index 3: l (U+006C)
// byte index 4: o (U+006F)
// byte index 5: , (U+002C)
// byte index 6: (U+0020)
// byte index 7: 世 (U+4E16) ← index 7, the next character is at byte 10!
// byte index 10: 界 (U+754C) ← 世 takes 3 bytes (7,8,9)
// Iterating per BYTE (not automatic — must convert to []byte)
for i, b := range []byte(s) {
fmt.Printf("byte index %d: 0x%02X\n", i, b)
}
Range over Maps #
Range over a map returns the key and value. The iteration order is not guaranteed — it differs every time the program runs:
prices := map[string]int{
"apple": 5000,
"mango": 8000,
"orange": 4500,
}
// Random order on every run
for k, v := range prices {
fmt.Printf("%s: Rp%d\n", k, v)
}
// If you need a consistent order, sort the keys first
import "sort"
keys := make([]string, 0, len(prices))
for k := range prices {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: Rp%d\n", k, prices[k])
}
// Output is always ordered: apple:5000, orange:4500, mango:8000
Range over Channels #
Range over a channel reads values until the channel is closed with close():
ch := make(chan int)
// Producer — sends values and closes the channel
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // IMPORTANT: close the channel so range stops
}()
// Consumer — range stops when the channel is closed
for v := range ch {
fmt.Println(v) // 0 1 2 3 4
}
break and continue
#
break — Exit the Loop
#
break stops the loop entirely and continues execution after the loop block:
// Find the first element matching a condition
target := 7
numbers := []int{3, 1, 4, 1, 5, 9, 2, 6, 7, 3}
found := -1
for i, v := range numbers {
if v == target {
found = i
break // stop immediately once found
}
}
if found >= 0 {
fmt.Printf("Found at index %d\n", found)
}
continue — Skip the Current Iteration
#
continue skips the rest of the loop body for the current iteration and goes straight to the next one:
// Process only elements meeting the criteria
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // skip even numbers
}
fmt.Println(i) // only prints odd numbers: 1 3 5 7 9
}
// Filter elements in a slice
data := []string{"apple", "", "mango", " ", "orange"}
var result []string
for _, s := range data {
s = strings.TrimSpace(s)
if s == "" {
continue // skip empty or whitespace-only strings
}
result = append(result, s)
}
fmt.Println(result) // [apple mango orange]
Labels for Nested Loops #
When fors are nested, unlabeled break and continue only affect the innermost loop. To affect an outer loop, use a label:
// Without a label — break only exits the inner loop
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break // only exits the j loop
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (1,0) (2,0) — the i loop keeps running
fmt.Println()
// With a label — break exits the labeled (outer) loop
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // exits the i loop too
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (0,1) (0,2) (1,0) — stops at i=1, j=1
fmt.Println()
// continue with a label — jump to the labeled loop's next iteration
outer2:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer2 // straight to the next i iteration
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (1,0) (2,0) — j=1 and j=2 are skipped because of continue outer2
Gotcha: Closures in Loops #
This is a classic, very common Go bug — one of the most frequently asked in interviews and on Stack Overflow. When you create a closure (anonymous function) inside a loop, that closure shares the same loop variable, rather than storing a copy of its value:
// ANTI-PATTERN: all goroutines print the same value
funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
funcs[i] = func() {
fmt.Println(i) // i is the SAME variable for all closures
}
}
for _, f := range funcs {
f()
}
// Output: 3 3 3 (not 0 1 2!)
// Because by the time f() is called, the loop is done and i = 3
// SOLUTION 1: create a copy of the variable per iteration (shadowing)
for i := 0; i < 3; i++ {
i := i // a new local variable shadowing the loop's i
funcs[i] = func() {
fmt.Println(i) // captures a different i each iteration
}
}
// SOLUTION 2: pass it as a function argument
for i := 0; i < 3; i++ {
funcs[i] = func(n int) func() {
return func() { fmt.Println(n) }
}(i) // i is evaluated now, its value is copied into n
}
// SOLUTION 3 (most common for goroutines): pass a parameter
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n) // n is a copy per call
}(i)
}
Starting with Go 1.22 (February 2024), the loop variable semantics changed: each loop iteration creates a new variable, so the closure problem above no longer happens by default for code compiled with Go 1.22+. Still, it’s important to understand this pattern for reading and maintaining older Go code.
Idiomatic Patterns #
Reverse Index Loops #
s := []int{1, 2, 3, 4, 5}
// Iterate from back to front
for i := len(s) - 1; i >= 0; i-- {
fmt.Println(s[i])
}
// Output: 5 4 3 2 1
// Remove elements from a slice while iterating — must go from the back
for i := len(s) - 1; i >= 0; i-- {
if s[i]%2 == 0 {
s = append(s[:i], s[i+1:]...) // remove even elements
}
}
Retry Loops with Exponential Backoff #
func withRetry(maxAttempts int, fn func() error) error {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
lastErr = fn()
if lastErr == nil {
return nil // success
}
if attempt == maxAttempts {
break // don't sleep after the last attempt
}
// Exponential backoff: 1s, 2s, 4s, ...
waitTime := time.Duration(1<<uint(attempt-1)) * time.Second
fmt.Printf("Attempt %d failed, retrying in %v: %v\n",
attempt, waitTime, lastErr)
time.Sleep(waitTime)
}
return fmt.Errorf("failed after %d attempts: %w", maxAttempts, lastErr)
}
Sliding Window #
// Calculate the moving average with a window size of 3
func movingAverage(data []float64, window int) []float64 {
if len(data) < window {
return nil
}
result := make([]float64, 0, len(data)-window+1)
sum := 0.0
// Calculate the sum for the first window
for i := 0; i < window; i++ {
sum += data[i]
}
result = append(result, sum/float64(window))
// Slide the window: add the new element, remove the old one
for i := window; i < len(data); i++ {
sum += data[i] - data[i-window]
result = append(result, sum/float64(window))
}
return result
}
Two-Pointer Pattern #
// Check whether a slice is a palindrome
func isPalindrome(s []int) bool {
left, right := 0, len(s)-1
for left < right {
if s[left] != s[right] {
return false
}
left++
right--
}
return true
}
fmt.Println(isPalindrome([]int{1, 2, 3, 2, 1})) // true
fmt.Println(isPalindrome([]int{1, 2, 3, 4, 5})) // false
Batch Processing #
// Process data in batches to avoid memory spikes
func processBatch(items []Item, batchSize int, fn func([]Item) error) error {
for i := 0; i < len(items); i += batchSize {
end := i + batchSize
if end > len(items) {
end = len(items) // the last batch may be smaller
}
batch := items[i:end]
if err := fn(batch); err != nil {
return fmt.Errorf("failed to process batch %d-%d: %w", i, end, err)
}
fmt.Printf("Finished batch %d-%d (%d items)\n", i, end, len(batch))
}
return nil
}
Comparison with Other Languages #
For developers coming from other languages, here’s a mapping of familiar loop patterns to Go:
// JAVA/C#: while
while (condition) { body }
// GO:
for condition { body }
// JAVA/C#: do-while
do { body } while (condition);
// GO: no direct equivalent — use:
for {
body
if !condition { break }
}
// PYTHON: for item in list
for item in items:
body
// GO:
for _, item := range items { body }
// JAVA: for (int i = 0; i < n; i++)
for (int i = 0; i < n; i++) { body }
// GO:
for i := 0; i < n; i++ { body }
// PYTHON: enumerate
for i, item in enumerate(items):
body
// GO:
for i, item := range items { body }
// PYTHON: zip
for a, b in zip(list1, list2):
body
// GO: no built-in zip — use the index
for i := 0; i < len(list1) && i < len(list2); i++ {
a, b := list1[i], list2[i]
body
}
Complete Example Program #
The following program simulates a simple analytics system processing sales data using various loop patterns:
package main
import (
"fmt"
"math"
"sort"
"strings"
)
type Sale struct {
Product string
Category string
Amount float64
Month int
}
// Calculate basic statistics using various loop patterns
func analyzeSales(sales []Sale) {
if len(sales) == 0 {
fmt.Println("No sales data")
return
}
// ── Pattern 1: Classic for — calculate the total
total := 0.0
for i := 0; i < len(sales); i++ {
total += sales[i].Amount
}
// ── Pattern 2: for-range — group by category
perCategory := make(map[string]float64)
for _, s := range sales {
perCategory[s.Category] += s.Amount
}
// ── Pattern 3: for-range over a map with sort for consistent output
fmt.Printf("=== Sales Analytics ===\n")
fmt.Printf("Total Revenue: Rp%.0f\n\n", total)
categories := make([]string, 0, len(perCategory))
for k := range perCategory {
categories = append(categories, k)
}
sort.Strings(categories)
fmt.Println("Sales per Category:")
for _, k := range categories {
pct := perCategory[k] / total * 100
bar := strings.Repeat("█", int(pct/5))
fmt.Printf(" %-15s Rp%8.0f %5.1f%% %s\n",
k, perCategory[k], pct, bar)
}
// ── Pattern 4: for-range with continue — filter data
fmt.Println("\nSales > Rp1,000,000:")
count := 0
for _, s := range sales {
if s.Amount <= 1_000_000 {
continue // skip the small ones
}
fmt.Printf(" %-20s Rp%.0f\n", s.Product, s.Amount)
count++
}
if count == 0 {
fmt.Println(" (none)")
}
// ── Pattern 5: for-range with break — find the biggest sales
sort.Slice(sales, func(i, j int) bool {
return sales[i].Amount > sales[j].Amount
})
fmt.Println("\nTop 3 Biggest Sales:")
for i, s := range sales {
if i >= 3 {
break // only take the top 3
}
fmt.Printf(" %d. %-20s Rp%.0f\n", i+1, s.Product, s.Amount)
}
// ── Pattern 6: reverse loop — show the 3 smallest sales
fmt.Println("\nTop 3 Smallest Sales:")
for i := len(sales) - 1; i >= len(sales)-3 && i >= 0; i-- {
rank := len(sales) - i
fmt.Printf(" %d. %-20s Rp%.0f\n", rank, sales[i].Product, sales[i].Amount)
}
// ── Pattern 7: nested loop with a label — find patterns per month per category
months := []int{1, 2, 3}
cats := []string{"Electronics", "Fashion"}
fmt.Println("\nSales per Month and Category:")
outer:
for _, month := range months {
for _, cat := range cats {
subtotal := 0.0
for _, s := range sales {
if s.Month == month && s.Category == cat {
subtotal += s.Amount
}
}
if subtotal > 0 {
fmt.Printf(" Month %d | %-15s Rp%.0f\n", month, cat, subtotal)
}
}
if month == 2 {
fmt.Println(" (only showing data through month 2)")
break outer
}
}
// ── Pattern 8: moving average for trends
perMonth := make([]float64, 13) // indexes 1-12 for months 1-12
for _, s := range sales {
if s.Month >= 1 && s.Month <= 12 {
perMonth[s.Month] += s.Amount
}
}
windowSize := 3
fmt.Printf("\n%d-Month Moving Average:\n", windowSize)
for i := windowSize; i <= 12; i++ {
sum := 0.0
for j := i - windowSize + 1; j <= i; j++ {
sum += perMonth[j]
}
avg := sum / float64(windowSize)
if avg > 0 {
fmt.Printf(" Month %2d (avg %d months): Rp%.0f\n", i, windowSize, avg)
}
}
// ── Pattern 9: infinite loop for a retry simulation
fmt.Println("\nDatabase Connection Simulation:")
maxRetry := 3
connected := false
attempt := 0
for {
attempt++
// Simulation: succeeds on the 2nd attempt
if attempt == 2 {
connected = true
fmt.Printf(" Attempt %d: connected successfully!\n", attempt)
break
}
fmt.Printf(" Attempt %d: failed, retrying...\n", attempt)
if attempt >= maxRetry {
fmt.Println(" Giving up after", maxRetry, "attempts")
break
}
}
_ = connected
// ── Final statistics
amounts := make([]float64, len(sales))
for i, s := range sales {
amounts[i] = s.Amount
}
mean := total / float64(len(sales))
variance := 0.0
for _, a := range amounts {
diff := a - mean
variance += diff * diff
}
variance /= float64(len(sales))
stddev := math.Sqrt(variance)
fmt.Printf("\nStatistics:\n")
fmt.Printf(" Number of transactions: %d\n", len(sales))
fmt.Printf(" Average : Rp%.0f\n", mean)
fmt.Printf(" Std Dev : Rp%.0f\n", stddev)
}
func main() {
sales := []Sale{
{"Pro Laptop", "Electronics", 15_000_000, 1},
{"Plain T-Shirt", "Fashion", 250_000, 1},
{"BT Earbuds", "Electronics", 1_500_000, 1},
{"Jeans", "Fashion", 450_000, 2},
{"4K Monitor", "Electronics", 5_000_000, 2},
{"Hoodie Jacket", "Fashion", 380_000, 2},
{"1TB SSD", "Electronics", 1_200_000, 3},
{"Formal Shirt", "Fashion", 320_000, 3},
{"Mech Keyboard", "Electronics", 2_500_000, 3},
{"Baseball Cap", "Fashion", 150_000, 3},
}
analyzeSales(sales)
}
Summary #
- Only one
forkeyword — Go has nowhile,do-while, orforeach; every loop pattern is expressed withfor.- Three forms of
for: the classic three-component (init; cond; post), condition-only (equivalent towhile), and the infinite loop (for {}).for-rangereturns the index/key and value for slices, arrays, maps, strings, and channels.rangegives a copy of the value — modifyingvdoesn’t affect the original collection; use the index to modify.rangeover strings iterates per rune (Unicode character), not per byte; indexes can jump for multi-byte characters.rangeover maps has no guaranteed order — sort the keys if you need consistent output.- Labeled
breakandcontinuecontrol outer loops from inside nested loops.- Closures in loops share the same variable — create a copy with
i := ior pass it as an argument to avoid bugs.- Go 1.22+ changed loop variable semantics so closures in loops no longer have this problem by default.
- Infinite loops with
for {}are a legitimate pattern for servers, daemons, and workers — stop them withbreak,return, or a channel signal.