Loops
Go Has Only One Loop Keyword
Go has a single loop construct: for. There is no while, no do-while, no foreach. Yet for covers every iteration pattern through three syntactic forms, plus the range clause. This deliberate minimalism means you never have to choose between loop types — there is always one obvious way to write a loop.
Go also provides break, continue, and labeled versions of both to handle nested-loop scenarios without requiring flag variables or helper functions.
Three Forms of for
C-Style (Init; Condition; Post)
package main
import "fmt"
func main() {
// Classic three-part for loop
for i := 0; i < 5; i++ {
fmt.Printf("i = %d\n", i)
}
// Count down
for i := 10; i > 0; i -= 3 {
fmt.Printf("countdown: %d\n", i)
}
// Multiple variables in init and post
for i, j := 0, 10; i < j; i, j = i+1, j-1 {
fmt.Printf("i=%d j=%d\n", i, j)
}
}
While-Style (Condition Only)
When you omit the init and post clauses, for behaves exactly like while in other languages:
package main
import "fmt"
func main() {
n := 1
for n < 100 {
n *= 2
}
fmt.Println("First power of 2 >= 100:", n)
// Reading user input until a valid value is received
attempts := 0
target := 42
guess := 0
for guess != target {
attempts++
// Simulating input for demonstration
guess = attempts * 7
fmt.Printf("Attempt %d: guess=%d\n", attempts, guess)
if attempts > 20 {
fmt.Println("Too many attempts, giving up")
break
}
}
if guess == target {
fmt.Printf("Found %d after %d attempts!\n", target, attempts)
}
}
Infinite Loop
Omit everything — for with no clauses loops forever until break or return:
package main
import (
"fmt"
"time"
)
func ticker(done <-chan struct{}) {
i := 0
for {
select {
case <-done:
fmt.Println("Ticker stopped")
return
default:
i++
fmt.Printf("Tick %d\n", i)
time.Sleep(500 * time.Millisecond)
if i >= 5 {
return
}
}
}
}
func main() {
done := make(chan struct{})
ticker(done)
}
range-Based Iteration
range iterates over built-in data structures. The exact values it yields depend on the type.
| Type | First value | Second value |
|---|---|---|
| Slice / Array | Index (int) | Element (copy) |
| String | Byte index (int) | Rune (int32) |
| Map | Key | Value |
| Channel | Value | (none) |
Use _ to discard either value.
Slice and Array
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana", "cherry", "date"}
// Both index and value
for i, fruit := range fruits {
fmt.Printf("[%d] %s\n", i, fruit)
}
// Index only
for i := range fruits {
fruits[i] = fruits[i] + "!"
}
fmt.Println("Modified:", fruits)
// Value only (discard index)
total := 0
numbers := []int{10, 20, 30, 40, 50}
for _, n := range numbers {
total += n
}
fmt.Println("Sum:", total)
}
String — Rune Iteration
range over a string yields Unicode code points (runes), not bytes. This correctly handles multi-byte characters.
package main
import "fmt"
func main() {
s := "Go언어"
fmt.Println("Byte-by-byte (len):")
for i := 0; i < len(s); i++ {
fmt.Printf(" s[%d] = %#x\n", i, s[i])
}
fmt.Println("\nRune-by-rune (range):")
for byteIdx, r := range s {
fmt.Printf(" byte[%d] = U+%04X '%c'\n", byteIdx, r, r)
}
}
// Byte indices for multi-byte runes are non-consecutive —
// range handles the UTF-8 decoding automatically.
Map
Map iteration order is intentionally randomized in Go. Never rely on insertion order.
package main
import (
"fmt"
"sort"
)
func main() {
population := map[string]int{
"Seoul": 9776000,
"Tokyo": 13960000,
"New York": 8336000,
"London": 8982000,
}
// Non-deterministic order
fmt.Println("Random order:")
for city, pop := range population {
fmt.Printf(" %-12s %d\n", city, pop)
}
// Sorted output
keys := make([]string, 0, len(population))
for k := range population {
keys = append(keys, k)
}
sort.Strings(keys)
fmt.Println("\nSorted order:")
for _, k := range keys {
fmt.Printf(" %-12s %d\n", k, population[k])
}
}
Channel
range over a channel receives values until the channel is closed:
package main
import "fmt"
func generate(n int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < n; i++ {
ch <- i * i
}
close(ch) // range stops when channel is closed
}()
return ch
}
func main() {
for sq := range generate(6) {
fmt.Println(sq)
}
}
break and continue
break exits the innermost loop immediately. continue skips the rest of the current iteration and advances to the next.
package main
import "fmt"
func main() {
// continue: skip even numbers
fmt.Println("Odd numbers < 10:")
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Print(i, " ")
}
fmt.Println()
// break: stop at first negative
data := []int{3, 7, 2, -1, 8, 4}
fmt.Println("\nValues before first negative:")
for _, v := range data {
if v < 0 {
break
}
fmt.Print(v, " ")
}
fmt.Println()
}
Labels — Breaking Nested Loops
Without labels, break and continue only affect the innermost loop. Labels let you target an outer loop directly.
package main
import "fmt"
func main() {
matrix := [][]int{
{1, 2, 3},
{4, -5, 6},
{7, 8, 9},
}
var found bool
var foundRow, foundCol int
outer:
for r, row := range matrix {
for c, val := range row {
if val < 0 {
found = true
foundRow, foundCol = r, c
break outer // exits both loops
}
}
}
if found {
fmt.Printf("Found negative value at row=%d col=%d\n", foundRow, foundCol)
}
// continue with a label skips to the next outer iteration
fmt.Println("\nRows with all positive values:")
rowLoop:
for r, row := range matrix {
for _, val := range row {
if val < 0 {
continue rowLoop // skip this entire row
}
}
fmt.Printf(" Row %d: %v\n", r, row)
}
}
Labels are plain identifiers followed by :. By convention, they are written in lowercase or camelCase (e.g., outer, rowLoop).
Practical Example: File Line Processing
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
type LineStats struct {
Total int
Blank int
Comment int
Code int
}
func analyzeSource(filename string) (LineStats, error) {
f, err := os.Open(filename)
if err != nil {
return LineStats{}, fmt.Errorf("open %s: %w", filename, err)
}
defer f.Close()
var stats LineStats
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
stats.Total++
switch {
case line == "":
stats.Blank++
continue
case strings.HasPrefix(line, "//"), strings.HasPrefix(line, "#"):
stats.Comment++
continue
default:
stats.Code++
}
}
if err := scanner.Err(); err != nil {
return stats, fmt.Errorf("scan %s: %w", filename, err)
}
return stats, nil
}
func main() {
// Create a temporary file for demonstration
content := `package main
import "fmt"
// main is the entry point
func main() {
// Print greeting
fmt.Println("Hello, Go!")
}
`
tmpFile, _ := os.CreateTemp("", "example*.go")
tmpFile.WriteString(content)
tmpFile.Close()
defer os.Remove(tmpFile.Name())
stats, err := analyzeSource(tmpFile.Name())
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
fmt.Printf("Total lines : %d\n", stats.Total)
fmt.Printf("Blank lines : %d\n", stats.Blank)
fmt.Printf("Comment lines : %d\n", stats.Comment)
fmt.Printf("Code lines : %d\n", stats.Code)
}
Practical Example: Concurrent Worker Pattern
package main
import (
"fmt"
"sync"
"time"
)
type Job struct {
ID int
Input int
}
type Result struct {
JobID int
Output int
}
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { // loop until jobs channel closes
time.Sleep(10 * time.Millisecond) // simulate work
results <- Result{
JobID: job.ID,
Output: job.Input * job.Input,
}
fmt.Printf("Worker %d processed job %d\n", id, job.ID)
}
}
func main() {
const numWorkers = 3
const numJobs = 9
jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)
var wg sync.WaitGroup
// Start workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send jobs
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Input: j}
}
close(jobs) // signal workers that no more jobs are coming
// Wait for all workers, then close results
go func() {
wg.Wait()
close(results)
}()
// Collect results
total := 0
for r := range results {
total += r.Output
}
fmt.Printf("\nSum of squares 1..%d = %d\n", numJobs, total)
}
Expert Tips
range copies slice elements. The second variable in for i, v := range slice is a copy of the element. Modifying v does not change the slice. Use the index form slice[i] when you need to mutate elements.
Prefer for range over explicit indexing for strings. Indexing a string (s[i]) gives you a byte, not a rune. for _, r := range s gives you the correct Unicode code point for every character, including multi-byte ones.
Close channels to signal completion. A range over a channel blocks until the channel is closed. Always ensure exactly one goroutine closes the channel, and only after all sends are complete.
Labels are not goto. Labels in Go only work with break and continue targeting loop constructs. They cannot jump to an arbitrary location in the code — the compiler enforces this.