Golang For Loop and `for range`: Syntax and Examples

Deepak Prasad
Tested on RHEL 10.2 with Go 1.27.0
Package go 1.27.0
Applies to Go language version 1.22+ for integer range and per-iteration loop variables; Go 1.23+ for iterator range
Privilege Normal user
Scope for and for range syntax, while-style and infinite loops, slices, maps, strings, integers, channels, iterator functions, break and continue, and per-iteration loop variables when the module go version is 1.22 or later. Does not cover full channel design, reflection, or the iter package in depth.
Related guides Golang break and continue
Go channels
Goroutines in Go
Golang struct
Getting started with Go

Go uses one looping keyword: for. Whether you want a counter, a while-style condition, an infinite loop, or a walk over a collection, you still write for.

The three classic shapes are:

go
for i := 0; i < 5; i++ {
	// three-part loop
}
go
for condition {
	// while-style loop
}
go
for {
	// infinite loop — exit with break, return, or similar
}

for range adds iteration over slices, arrays, maps, strings, channels, integers (Go 1.22+), and iterator functions (Go 1.23+). The sections below follow that order from everyday counters to modern iterator syntax.

Pick the shape that matches what you are iterating. Counters and manual index control use the three-part for. Collection walks use for range. When you only need to repeat until a condition flips, use while-style for. The decision table in your head is simpler than in languages that split for, while, and do-while across different keywords.


Basic three-part for loop

A three-part for has initialization, condition, and post statements. Go does not use C-style parentheses around the three clauses.

go
package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		fmt.Println(i)
	}
}
Output

Sample output:

output
0
1
2
3
4

The init runs once, the condition is checked before each iteration, and the post runs after each body execution. When the condition is false, the loop stops.

A variable declared in the init clause belongs to the for statement, including its condition, post statement, and body. It is not available after the loop ends unless you declared it outside the for first.


Use for like a while loop

Go has no while keyword. Omit the init and post clauses and keep only the condition:

go
package main

import "fmt"

func main() {
	n := 0
	for n < 5 {
		n++
	}
	fmt.Println(n)
}
Output

Sample output:

output
5

The loop runs while n < 5 holds, same role a while loop plays in other languages.

You can also write for ; condition; { } with empty init and post, but most code omits those semicolons entirely when only the condition matters.


Create an infinite loop

An empty for with no condition repeats until something inside stops it:

go
package main

import "fmt"

func main() {
	n := 0
	for {
		fmt.Println("tick", n)
		n++
		if n >= 3 {
			break
		}
	}
}
Output

Sample output:

output
tick 0
tick 1
tick 2

break exits the loop. You can also return from a function or stop work when a context is cancelled in concurrent code. Run infinite loops only when you have a clear exit path.

In servers and workers, infinite loops usually wrap select on channels or block on ctx.Done() rather than spinning forever with no I/O.


Loop over slices and arrays with range

for range on a slice or array yields an index and a value on each iteration. Arrays and slices share the same range syntax; the index is always valid for the length of the collection you pass in.

go
package main

import "fmt"

func main() {
	values := []int{10, 20, 30}
	for i, value := range values {
		fmt.Printf("i=%d value=%d\n", i, value)
	}
}
Output

Sample output:

output
i=0 value=10
i=1 value=20
i=2 value=30

Index, value, and the blank identifier

Ignore the index with _ when you only need values:

go
for _, value := range values {
	fmt.Println(value)
}

When you only need indices, omit the value variable:

go
for i := range values {
	fmt.Println(i)
}

_ is the blank identifier. It discards a value you do not need while keeping the two-value range form valid.

for range on a nil slice still runs zero times. It does not panic, which matches how len(nil) is zero for slices.

Modify slice elements correctly

The value in for _, v := range values is a copy of each element. Changing v does not update the slice:

go
package main

import "fmt"

func main() {
	values := []int{1, 2, 3}
	for _, v := range values {
		v++
	}
	fmt.Println("wrong:", values)

	for i := range values {
		values[i]++
	}
	fmt.Println("right:", values)
}
Output

Sample output:

output
wrong: [1 2 3]
right: [2 3 4]

Use the index form values[i] when you need to mutate elements in place.

The same copy rule applies to ranging over structs in a slice: for _, u := range users copies each struct value. Large structs sometimes use index access or pointers when updates must persist.


Range over maps and strings

Maps

Map range yields a key and value per iteration:

go
package main

import "fmt"

func main() {
	m := map[string]int{"a": 1, "b": 2, "c": 3}
	for key, value := range m {
		fmt.Printf("%s=%d\n", key, value)
	}
}
Output

Each key and value pair appears once per iteration. The order you see is not part of the language contract.

You can range with only the key when values are unnecessary:

go
for key := range m {
	fmt.Println(key)
}

Map iteration order

Map iteration order is not specified and is not guaranteed to remain the same from one run to the next. Do not assume keys arrive sorted or in insertion order. When deterministic order matters, collect keys into a slice, sort that slice with sort.Strings or sort.Slice, then loop over the sorted keys.

Two consecutive for range loops over the same map may print keys in different orders even when the map did not change.

String byte offsets and runes

String range yields a byte offset and a Unicode code point (rune):

go
package main

import "fmt"

func main() {
	s := "Hi世!"
	for i, r := range s {
		fmt.Printf("offset=%d rune=%U char=%c\n", i, r, r)
	}
}
Output

Sample output:

output
offset=0 rune=U+0048 char=H
offset=1 rune=U+0069 char=i
offset=2 rune=U+4E16 char=世
offset=5 rune=U+0021 char=!

The first range value is the byte index of each Unicode code point in the UTF-8 string, not a universal character index. Use a normal byte-index loop when you specifically need raw UTF-8 bytes, for example for i := 0; i < len(s); i++ with s[i].

occupies three UTF-8 bytes, so the next rune starts at byte offset 5 rather than 3.


Range over integers, channels, and iterator functions

Integers in Go 1.22+

Since Go 1.22, you can range over an integer:

go
package main

import "fmt"

func main() {
	for i := range 5 {
		fmt.Println(i)
	}
}
Output

Sample output:

output
0
1
2
3
4

Values run from 0 through n-1. When n <= 0, the loop body does not execute.

Integer range is a shorter alternative to for i := 0; i < n; i++ when you only need values from zero through n-1. Go 1.22 added integer range; the traditional three-clause form remains fully normal Go when you need a different step or start value.

Channels

Receiving with for value := range ch reads values until the channel is closed:

go
package main

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)
	for v := range ch {
		println(v)
	}
}
Output

Sample output:

output
1
2
3

Only the sender side should close a channel when your protocol requires it. Ranging over a nil channel blocks forever because no values ever arrive and the channel is never closed.

Channel loops are the glue between goroutines; see Go channels for send and receive patterns beyond this range sketch.

Iterator functions in Go 1.23+

Go 1.23 lets range iterate over certain iterator functions. The language supports functions with these shapes:

text
func(func() bool)
func(func(K) bool)
func(func(K, V) bool)

The standard iter package defines convenient Seq and Seq2 types for one- and two-value iterators. Here count returns iter.Seq[int]:

go
package main

import (
	"fmt"
	"iter"
)

func count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 0; i < n; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	for v := range count(3) {
		fmt.Println(v)
	}
}
Output

Sample output:

output
0
1
2

This is enough to recognize iterator range in modern Go. The yield callback lets the iterator stop early when the consumer breaks out of the loop. See the iter package and the language specification for full yield signatures and iter.Seq2 variants that yield pairs.


break, continue, and labeled loops

break exits the innermost for, switch, or select. continue skips to the next iteration of the innermost for:

go
package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		if i == 2 {
			continue
		}
		if i == 4 {
			break
		}
		fmt.Println(i)
	}
}
Output

Sample output:

output
0
1
3

A labeled break can exit an outer loop when you are nested:

go
outer:
for i := 0; i < 3; i++ {
	for j := 0; j < 3; j++ {
		if j == 1 {
			break outer
		}
	}
}

For labeled continue, select interaction, and outer-loop patterns, see Golang break and continue.

break and continue apply to the innermost active loop by default. Labels add a name when nested loops or a select inside a for need a wider exit or skip target.


Loop variables and closures since Go 1.22

Older Go code often warned about this pattern:

go
for _, v := range values {
	go func() {
		fmt.Println(v)
	}()
}

Before Go language version 1.22, every goroutine could observe the same v after the loop finished. Starting with Go language version 1.22, loop variables declared by the loop get a distinct variable for each iteration. In modules, this normally means the go directive in go.mod is 1.22 or later. Each goroutine then typically sees the correct value:

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	values := []int{1, 2, 3}
	var wg sync.WaitGroup
	for _, v := range values {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(v)
		}()
	}
	wg.Wait()
}
Output

Sample output (order varies):

output
3
1
2

You may still see v := v in legacy tutorials. That workaround is generally unnecessary for variables declared directly by the loop in modules using go 1.22 or later.

Closure capture can still bite when the loop assigns to a variable declared outside the loop, or when goroutines share other mutable state. Treat outer variables and shared references separately from loop-declared names.

If you copy a loop variable into a struct field or slice before launching a goroutine, you are working with a different variable than the one the loop declares. The per-iteration rule applies to identifiers introduced by the for statement itself, such as v in for _, v := range.


Common for and range mistakes

Mistake Correct understanding
Looking for a while keyword Use for condition
Assuming map order Map iteration order is unspecified
Modifying the range value and expecting the slice element to change Modify via index: values[i]
Treating string range index as character number It is a byte offset into UTF-8
Ranging a nil channel Blocks forever
Applying pre-Go 1.22 closure advice blindly Modules using Go language version 1.22+ give loop-declared variables per-iteration semantics
Trying range directly on an ordinary struct Struct values are not rangeable

References


Summary

Every Go loop uses for. Counters use the three-part form, while-style loops drop init and post, and for { } repeats until break, return, or another exit stops it. for range covers slices, maps, strings, channels, integers since Go 1.22, and iterator functions since Go 1.23.

The pitfalls that matter day to day are slice mutation through the range value copy, unspecified map order, and string offsets versus runes. Integer range counts from zero to n-1, and channel range ends when the channel closes. A []any slice loops like any other slice; type switches belong to interface tutorials, not loop syntax.

Modules using Go language version 1.22+ give loop-declared variables per-iteration semantics, which fixes the classic goroutine capture bug for for range, but shared outer state still needs care. Ordinary structs are not rangeable; walk a slice of structs or reach for reflection only when dynamic field access is truly required.

For labeled breaks, continue on outer loops, and select inside loops, use the dedicated break and continue guide linked above. Together, classic for, modern integer and iterator range, and per-iteration loop variables in modules at Go 1.22+ give you a complete picture of looping in current Go without hunting for a separate while keyword.


Frequently Asked Questions

1. Does Go have a while loop?

No separate while keyword. Write for condition with the init and post clauses omitted, for example for n < 5 { n++ }. A for loop with only a condition is the idiomatic while-style loop in Go.

2. How do I ignore the index in a for range loop?

Use the blank identifier as the first variable, for example for _, value := range values. You can also use for i := range values when you only need the index.

3. Can I use range with an integer in Go?

Yes, since Go 1.22. for i := range n iterates i from 0 through n-1. When n is zero or negative, the loop body does not run.

4. Can I directly range over a struct?

Ordinary struct values are not rangeable. Loop over a slice of structs with for range, or use reflection only when you genuinely need dynamic field inspection at runtime.

5. Is map iteration order guaranteed in Go?

No. Map iteration order is not specified and is not guaranteed to stay the same between iterations. Collect and sort keys when you need deterministic order.

6. Why does changing the range value not modify my slice?

The range value variable is a copy of each element. Incrementing v in for _, v := range values changes the copy, not the slice slot. Modify values[i] using the index from for i := range values.

7. Did Go fix the loop-variable closure problem?

For variables declared by the loop itself in modules whose go directive is 1.22 or later, each iteration gets its own variable, which fixes the classic goroutine capture bug with range for those declarations. Pre-existing variables assigned from the loop can still be shared; do not assume every closure pattern is safe.
Antony Shikubu

Systems Integration Engineer

Highly skilled software developer with expertise in Python, Golang, and AWS cloud services.

  • Go (programming language)
  • Python (programming language)
  • Amazon Web Services