| 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:
for i := 0; i < 5; i++ {
// three-part loop
}for condition {
// while-style loop
}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.
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}Sample output:
0
1
2
3
4The 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:
package main
import "fmt"
func main() {
n := 0
for n < 5 {
n++
}
fmt.Println(n)
}Sample output:
5The 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:
package main
import "fmt"
func main() {
n := 0
for {
fmt.Println("tick", n)
n++
if n >= 3 {
break
}
}
}Sample output:
tick 0
tick 1
tick 2break 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.
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)
}
}Sample output:
i=0 value=10
i=1 value=20
i=2 value=30Index, value, and the blank identifier
Ignore the index with _ when you only need values:
for _, value := range values {
fmt.Println(value)
}When you only need indices, omit the value variable:
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:
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)
}Sample 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:
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)
}
}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:
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):
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)
}
}Sample 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:
package main
import "fmt"
func main() {
for i := range 5 {
fmt.Println(i)
}
}Sample output:
0
1
2
3
4Values 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:
package main
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
println(v)
}
}Sample output:
1
2
3Only 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:
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]:
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)
}
}Sample output:
0
1
2This 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:
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
if i == 2 {
continue
}
if i == 4 {
break
}
fmt.Println(i)
}
}Sample output:
0
1
3A labeled break can exit an outer loop when you are nested:
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:
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:
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()
}Sample output (order varies):
3
1
2You 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
- Language specification — For statements
- Go Tour — For
- Go Tour — Range
- Go 1.22 release notes — range over integers and loop variables
- Go 1.23 release notes — range over iterators
- Package iter
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.

