← Fundamentals
Guided lesson 7 questions · Go only

Go’s one loop and range

Four forms of one keyword, what range copies, which variables each pass gets, and the order of a map.

Go has one loop keyword, for, in four forms: three parts, a condition only, no condition, and range. range evaluates what it reads once, before the first pass, and each pass gets its own copies of the index and the element. A map, which stores values under keys, gives its entries in no fixed order.

Written for Go.

Before you begin

Why this matters

Most Go loops are range loops. If you know when range reads what it loops over, and what it copies, you can predict which changes a loop sees. You can also read older Go code, whose loops shared one variable between passes.

What this helps you reason about

  • Predict how many times the code after range runs, and whether a loop sees a change made during the loop.
  • Explain why functions made in a loop each see their own value since Go 1.22, and spot older code that relied on one shared variable.
  • Spot output whose order changes from one run to the next, and make the order fixed.

In most languages, for, while, and for-each are different loops. Go writes all of them with one keyword, for. For loops and iteration protocols showed that range gives an index and a copy of each element of a slice. This lesson follows range further, at a car rental desk: what range reads once, what it copies, which variables each pass gets since Go 1.22, and what it does with text, keys, and functions.

Already know this? The short version
  • Go’s for has four forms: for init; cond; post, for cond, for { … } ended by break or return, and for … range over an integer, array, pointer to an array, slice, string, map, channel, or iterator function; labels work with every form.
  • range evaluates its expression once, before the first pass: a call in it runs once, appends are not read, and an array is copied whole, so writes to its later elements are not seen; range over &arr or arr[:] sees them.
  • Range variables are copies: v.Field = x changes the copy, so write s[i].Field. Since Go 1.22, each pass has new variables, so closures, goroutines, and &v keep the values of their own pass; before Go 1.22, all passes shared one variable.
  • A map gives its entries in an order that changes between runs. Deleting an entry not yet reached is safe, and that entry is not produced; an entry added during the loop may or may not be produced. The section “What goes wrong with range in real programs?” covers struct copies, &v, map order in output and tests, and large arrays.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
for loop
Go’s only loop. for start; condition; update { body } counts, for condition { body } is a while loop, and for … range reads the elements of a slice, array, string, or map.
range variable
The variable that a for ... range loop fills with each element in turn, such as item in for _, item := range items. It holds a copy of the element, not the element itself.
struct
A value that groups named fields, such as Card{Total: 5}. Assigning or passing a struct copies every field.
array
A value with a fixed number of elements, such as [3]int. The length is part of the type, and assigning or passing an array copies every element.
map
A built-in collection that stores values under keys, such as map[string]int. A map value refers to entries stored elsewhere, so a copy of it reaches the same entries.
function literal
A function written as a value, without a name, such as func(n int) int { return n + 1 }. It can be stored in a variable, passed, or returned.
closure
A function literal together with the variables from the surrounding function that it uses. Those variables last as long as the closure does.
iterator function
A function that range can read since Go 1.23. It takes a function, usually named yield, and calls it once for each value; yield returns false when the loop ends early.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Four forms

How can one keyword make every loop?

For loops and iteration protocols and While loops and termination showed the forms of for one at a time. Go has no other loop keyword, so here are all four together. for i := 0; i < 3; i++ counts, with a start, a condition, and an update. for fuel < 50 has a condition only, and works as a while loop. for with nothing after it has no condition, and runs until a break or a return ends it. for i, v := range x reads x one element at a time. break, continue, and labels work with every form, as Break, continue, and early return showed.

The range form has one rule that the other forms do not have. The expression after range is evaluated once, before the first pass. The condition of a three-part loop is evaluated before every pass. So for i := range carsReady() calls carsReady once. If carsReady returns 3, the loop then gives i the values 0, 1, and 2, because range over an integer n counts from 0 up to n − 1 (since Go 1.22).

A function, as in From source to a running program, is a named group of lines; carsReady below prints a line and returns 3.

main.go
fuel := 10
for fuel < 50 { // a condition only
    fuel += 15
}
for { // no condition: only break or return ends it
    fuel -= 20
    if fuel < 0 {
        break
    }
}
for i := range carsReady() { // carsReady runs once
    fmt.Println("hand over car", i)
}

Go’s for has four forms. In the range form, the expression after range is evaluated once, before the first pass.

Coming from C or Java: for (i = 0; i < count(); i++) calls count() before every pass, and so does Go’s three-part for; only range evaluates its expression once.

Precisely: what range accepts

The specification lists what range can read: an integer, an array, a pointer to an array, a slice, a string, a map, a channel, and, since Go 1.23, a function of one of three iterator shapes. The three shapes are func(yield func() bool), func(yield func(V) bool), and func(yield func(K, V) bool). Range over a channel or a function has no fixed number of passes: it goes on until the channel is closed or the function returns.

Experiment

Count how often range calls its function

range evaluates the expression after it once, before the first pass, and keeps the result. The condition of a three-part for is evaluated again before each pass, so a call inside it runs each time, and one more time for the test that ends the loop.

Why it matters A call after range runs once. A call in the condition of a three-part loop runs before every pass, so keep slow work out of the condition.

Predict how many times counting cars is printed. Then change for i := range carsReady() { to for i := 0; i < carsReady(); i++ { and predict again.

package main

import "fmt"

func carsReady() int {
    fmt.Println("counting cars")
    return 3
}

func main() {
    for i := range carsReady() {
        fmt.Println("hand over car", i)
    }
}
What to look for

The first stop after main’s for line is inside carsReady, with main waiting below it in the frame list. carsReady returns 3, and the stepper does not enter carsReady again: the next stops are the Println line with i 0, 1, and 2. The run prints counting cars once, then hand over car 0, 1, and 2. After the edit, the stepper enters carsReady before every pass and once more at the end, when 3 < 3 is false. The run prints counting cars four times, between the car lines.

Expected output

counting cars
hand over car 0
hand over car 1
hand over car 2

02 / Copies

What does range copy?

For loops and iteration protocols showed that the range variable holds a copy of the element. The same is true when the element is a struct: a value that groups named fields, such as Car{Plate: "KA-31", Km: 41200}. Copying a struct copies every field, so car.Km = 0 inside the loop changes the copy, not the car in the slice. Write cars[i].Km = 0 instead. Records, structs, and objects, a later lesson, covers structs in full.

An array is copied in a larger way. An array is a value with a fixed number of elements, such as [3]int, and assigning it copies every element. range evaluates the array once, and that value is a copy of the whole array. So when the loop body changes a later element, the range variable still shows the old value when the loop reaches it.

A slice is different. A slice refers to elements stored elsewhere, so range copies only the slice and reads each element as it is when its pass begins. To read an array as it changes, range over fuel[:], a slice of all of fuel’s elements, or over &fuel, a pointer: a value that leads to fuel itself.

main.go
cars := []Car{{"KA-31", 41200}, {"LU-07", 9800}}
for i, car := range cars {
    car.Km = 0     // changes the copy only
    cars[i].Km = 0 // changes the car in the slice
}
fuel := [3]int{20, 35, 50}
for i, litres := range fuel { // reads a copy of fuel
    if i == 0 {
        fuel[2] = 10
    }
    fmt.Println(litres) // 20, 35, 50
}

Each range variable is a copy of one element, and range over an array reads a copy of the whole array. Write through the index, and range over arr[:] or &arr to see changes.

Coming from Java: An enhanced for over an array of objects gives references, so car.km = 0 changes the object in the array; a Go range variable of a struct type is a separate copy.

Coming from C: A C array used as a value turns into a pointer to its first element; a Go array is a value, and range copies all of it.

Precisely: what is evaluated

The specification says the range expression is evaluated once and each element is assigned to the iteration variables. For a slice, the value evaluated is the slice itself: a pointer to the elements, a length, and a capacity. So element reads see writes made during the loop, while the number of passes stays fixed. For an array, the value evaluated is the array. For a pointer to an array, it is the pointer, so element reads see the array as it is.

Experiment

Change an array while range reads it

range evaluated fuel once, before the first pass. An array value is all of its elements, so range kept a copy of all three, and each litres was read from that copy. fuel[:] is a slice that refers to fuel’s own elements, so range copies only the slice, and each pass reads the element as it is at that moment.

Why it matters When the loop body changes an array that range reads, the loop does not see the change. Range over arr[:] or &arr, or read arr[i].

In the first pass, the loop sets fuel[2] to 10. Predict the litres printed for car 2. Then change range fuel to range fuel[:] and predict again.

package main

import "fmt"

func main() {
    // litres of fuel in the three cars at the desk
    fuel := [3]int{20, 35, 50}
    for i, litres := range fuel {
        if i == 0 {
            fuel[2] = 10 // car 2 comes back almost empty
        }
        fmt.Println("car", i, "has", litres, "litres")
    }
    fmt.Println("now:", fuel)
}
What to look for

At the Println line of the first pass, fuel shows [3]int [20,35,10]: the write happened. Step to the third pass. litres shows 50, while fuel still shows 10 at index 2. The run prints 20, 35, and 50 litres, then now: [20 35 10]. The copy that range reads is not in the variables list; litres is the only sign of it. After the edit, litres shows 10 in the third pass, and the run prints car 2 has 10 litres.

Expected output

car 0 has 20 litres
car 1 has 35 litres
car 2 has 50 litres
now: [20 35 10]

03 / Maps

In what order does range read a map?

A map stores values under keys. map[string]int{"KA-31": 41200} stores 41200 under the key "KA-31", and km["KA-31"] reads it. Maps, dictionaries, and hashing, a later lesson, shows how a map finds a key. Here one thing matters. for plate, driven := range km gives each key and its value once, in an order that the language does not fix. Go even chooses a different order in different runs, on purpose, so that no program comes to depend on one order. With one variable, for plate := range km gives the keys only.

Changing the map during the loop has two rules. Deleting is allowed: delete(km, plate) removes an entry, and an entry deleted before the loop reaches it is not produced. Adding is allowed too, but an entry added during the loop may be produced or may be skipped, and that can differ between runs. Do not count on either.

When the order matters, as on a screen or in a test, put the keys in a slice, sort the slice, and range over it. sort.Strings sorts a slice of strings in place.

main.go
km := map[string]int{"ZE-12": 60500, "KA-31": 41200}
var plates []string
for plate := range km { // one variable: the keys
    plates = append(plates, plate)
}
sort.Strings(plates) // KA-31, ZE-12
for _, plate := range plates {
    fmt.Println(plate, km[plate])
}

range over a map gives each entry once, in an order that changes between runs. Deleting during the loop is safe; to print in a fixed order, sort the keys first.

Coming from Python: A dict keeps the order in which keys were added, and a loop follows that order; a Go map keeps no order.

Coming from Java: A HashMap’s order is unspecified too, but it usually repeats from run to run; Go changes it on purpose, so a test that passed once can fail on the next run.

Precisely: what is promised

The specification says the order is not specified and is not guaranteed to be the same from one loop to the next. The runtime chooses a random starting point for each loop. fmt.Println(km) is different: since Go 1.12, fmt sorts a map’s keys before printing it, so printing a whole map gives the same text every time. Range over a nil map makes zero passes.

Experiment

Delete entries while range reads a map

range over a map gives each entry once, but the language does not fix the order, and Go picks a new order for each loop. Deleting the current entry is allowed and does not disturb the loop. The number of cars left does not depend on the order, so it is the same in every run.

Why it matters A loop over a map may delete entries and may compute counts and sums. Anything that prints or compares in loop order needs sorted keys.

Cars that have driven more than 40000 km are retired. Predict how many cars are left. Then add fmt.Println(plate) as the first line inside the loop, run the program twice, and compare the order of the plates.

package main

import "fmt"

func main() {
    km := map[string]int{
        "KA-31": 41200,
        "LU-07": 9800,
        "ZE-12": 60500,
        "BO-44": 12000,
    }
    for plate, driven := range km {
        if driven > 40000 {
            delete(km, plate) // retire the car
        }
    }
    fmt.Println(len(km), "cars left")
}
What to look for

The stops on the if line show plate and driven for one entry at a time. In one recorded run the order was LU-07, ZE-12, BO-44, KA-31, while km listed the entries as KA-31, LU-07, ZE-12, BO-44: the stepper lists a map in its own storage order, which is not the order range uses. When plate is ZE-12, delete removes it, and at the next stop km has three entries. The run prints 2 cars left. After the edit, the plates come out in an order that can change from one run to the next; two runs gave KA-31, LU-07, ZE-12, BO-44 and BO-44, KA-31, LU-07, ZE-12. The last line is 2 cars left every time.

Expected output

2 cars left

04 / Each pass

Does each pass get its own variables?

Each pass of a loop sets its loop variables. Since Go 1.22, each pass declares new ones: the plate of pass 1 and the plate of pass 2 are two different variables. This matters only when something keeps a variable after its pass ends.

A function literal is a function written as a value, without a name, such as func() { fmt.Println("return", plate) }. When it uses plate, it keeps that variable itself, not a copy of its value. Together they form a closure. Closures and captured variables, a later lesson, covers closures in full. Below, calls is a slice of functions, each pass appends one closure, and call() runs one. Because each pass has its own plate, the three closures print three different plates.

Before Go 1.22, a loop had one set of variables, and every pass wrote into them. All three closures kept the same plate, which held ZE-12 after the loop, so they printed ZE-12 three times. You will meet this in older code: a line plate := plate inside the loop made a new variable for each pass by hand. Go 1.22 and later do not need it. A goroutine, a function running on its own and started with go, had the same problem when it used a loop variable, and the same rule now fixes it.

main.go
var calls []func()
for _, plate := range []string{"KA-31", "LU-07", "ZE-12"} {
    calls = append(calls, func() { fmt.Println("return", plate) })
}
for _, call := range calls {
    // KA-31, LU-07, ZE-12; before Go 1.22: ZE-12 three times
    call()
}

Since Go 1.22, each pass of a for loop has its own variables, so a closure or goroutine keeps the values of its own pass. Before Go 1.22, all passes shared one set of variables.

Coming from JavaScript: Go made the same change as var to let in a for loop: since Go 1.22, each pass gets a new variable, as with let.

Precisely: which rule a file gets

The rule depends on the Go language version of the code, not on the installed toolchain. In a module, that is the go line in go.mod: a module that says go 1.21 keeps the old rule even when built with a newer Go. A file can ask for an older version with a //go:build go1.21 line at its top. The stepper builds with Go 1.27 and no go.mod, so the new rule applies. The three-part form also gets a new i for each pass: before the update, Go copies i into the new variable. The go vet loopclosure check reported closures that used loop variables in the old rule.

Experiment

Keep one function from each pass

Since Go 1.22, each pass declares a new plate, and each function keeps the plate of its own pass. The go1.21 line makes the compiler use the rules of Go 1.21 for this file: one plate for the whole loop, written again by every pass. All three functions keep that one variable, and after the loop it holds ZE-12.

Why it matters In Go 1.22 or later, a function or goroutine made in a loop sees the values of its own pass. In older code, look for plate := plate inside the loop, which made a new variable by hand.

Predict the three lines. Then add the line //go:build go1.21 at the very top of the program, with an empty line after it, and predict again.

package main

import "fmt"

func main() {
    var calls []func()
    for _, plate := range []string{"KA-31", "LU-07", "ZE-12"} {
        calls = append(calls, func() { fmt.Println("return", plate) })
    }
    for _, call := range calls {
        call()
    }
}
What to look for

While the first loop runs, calls lists one function per pass, each shown with the plate it keeps: [{plate: "KA-31"}], then [{plate: "KA-31"}, {plate: "LU-07"}]. The run prints return KA-31, return LU-07, return ZE-12. After the edit, every function in calls shows the plate of the current pass, and after the loop all three show ZE-12: they keep one shared variable. The run prints return ZE-12 three times. The stepper also enters each function, as main.main.func1. At its first stop there, plate can show “unreadable” or the plate of the call before; call in main’s frame shows which function is running.

Expected output

return KA-31
return LU-07
return ZE-12

05 / Other sources

What else can range read?

range reads more than slices and arrays.

Over a string, range gives runes, not bytes. The first variable is the byte position where each rune starts, so it jumps by 2 over ö in "Köln", as Text, bytes, and Unicode showed. Use it as a position in the string, never as a count of letters.

Over a channel, range receives values until the channel is closed. Channels belong to a later topic on goroutines.

Over a function: since Go 1.23, range can read an iterator function, a function that hands out values one at a time. It takes another function, usually named yield, and calls it once for each value. The standard library returns iterator functions. slices.Backward(s) gives the index and element of s from the last element to the first. maps.Keys(m) gives the keys of a map, in the map’s changing order. slices.Sorted(maps.Keys(km)) collects those keys into a sorted slice: a shorter form of the sorting code in the map section.

main.go
for i, r := range "Köln" {
    fmt.Println(i, string(r)) // 0 K, 1 ö, 3 l, 4 n
}
plates := []string{"KA-31", "LU-07", "ZE-12"}
for i, p := range slices.Backward(plates) {
    fmt.Println(i, p) // 2 ZE-12, 1 LU-07, 0 KA-31
}
for _, plate := range slices.Sorted(maps.Keys(km)) {
    fmt.Println(plate) // in sorted order
}

Over a string, range gives byte positions and runes; over a channel, values until it is closed; over an iterator function, each value it passes to yield.

Precisely: writing an iterator function

evenBays below returns a function of type func(yield func(int) bool), which the package iter names iter.Seq[int]. In for b := range evenBays(7), the loop body becomes yield: each yield(b) runs the body once with that b. When the body runs break, yield returns false, and the iterator must stop. Calling yield again after it returned false makes the program panic. With the break at 4, the run prints yield 0, bay 0, yield 2, bay 2, yield 4.

main.go
func evenBays(n int) func(yield func(int) bool) {
    return func(yield func(int) bool) {
        for b := 0; b < n; b += 2 {
            fmt.Println("yield", b)
            if !yield(b) {
                return // the loop ended early
            }
        }
    }
}

for b := range evenBays(7) {
    if b == 4 {
        break
    }
    fmt.Println("bay", b)
}

06 / In production

What goes wrong with range in real programs?

Range loops in real programs fail in four ways that the compiler accepts.

Changing a struct copy. for _, b := range bookings { b.Paid = true } changes nothing that lasts. Write bookings[i].Paid = true. When the slice holds pointers, as []*Booking, b is a copy of a pointer, so b.Paid = true does change the booking.

Taking &v of the range variable. Since Go 1.22, &car points to that pass’s copy, so the value read through it is right, but a change through it never reaches the slice. Before Go 1.22, &car was the same address in every pass, so every kept pointer showed the last element. Point at &cars[i] instead, as below.

Printing in map order. Log lines and test output built in range order change from run to run, so a test that compares them with one expected text passes on some runs and fails on others. Sort the keys first, or compare the results as a map.

Ranging over a large array value. for _, v := range grid, where grid is an array such as [100000]int, works on a copy of every element. It reads old values, as in the copies section, and the copy can also cost time. Range over &grid or grid[:].

main.go
var most *Car
for i := range cars {
    if most == nil || cars[i].Km > most.Km {
        most = &cars[i] // points into the slice
    }
}
most.InService = true // changes cars[i]

Write through the index, point at s[i] rather than at the range variable, sort map keys before output, and range over &arr instead of a large array value.

Precisely: tools that help

go vet’s copylocks check reports a range variable that copies a value holding a sync.Mutex. No standard check reports a change to a struct copy. For tests, comparing two maps with maps.Equal, or sorting before comparing, keeps the result the same in every run.

Experiment

Keep a pointer to the range variable

car is a copy of one element, and since Go 1.22 each pass has its own car. &car points to that copy, so most leads to a copy of ZE-12, not to the element in cars. &cars[i] points to the element itself.

Why it matters A pointer to a range variable reads the right value since Go 1.22, but a change through it never reaches the slice. Point at s[i] when the change must stay.

The loop keeps a pointer to the car that has driven furthest, and the line after the loop marks that car for service. Predict whether the last line shows ZE-12 in service. Then change most = &car to most = &cars[i] and predict again.

package main

import "fmt"

type Car struct {
    Plate     string
    Km        int
    InService bool
}

func main() {
    cars := []Car{{"KA-31", 41200, false}, {"ZE-12", 60500, false}}
    var most *Car // the car that has driven furthest
    for i, car := range cars {
        fmt.Println("check", i, car.Plate)
        if most == nil || car.Km > most.Km {
            most = &car
        }
    }
    most.InService = true
    fmt.Println(cars)
}
What to look for

most first shows *main.Car nil. After the first pass it shows an address, → and the KA-31 car. After the second pass it shows a different address and the ZE-12 car: each pass had its own car variable. The addresses change from run to run. most.InService = true makes most show InService: true, but cars still shows false for both cars. The run prints [{KA-31 41200 false} {ZE-12 60500 false}]. After the edit, most points into cars, and the run prints [{KA-31 41200 false} {ZE-12 60500 true}].

Expected output

check 0 KA-31
check 1 ZE-12
[{KA-31 41200 false} {ZE-12 60500 false}]

07 / Try it

Can you predict what this loop prints?

Decide what each car variable holds before you add anything. The next lesson, Function calls, shows what happens when a function is called.

main.go
package main

import "fmt"

type Car struct {
    Plate string
    Km    int
}

func main() {
    cars := [3]Car{{"KA-31", 100}, {"LU-07", 200}, {"ZE-12", 300}}
    total := 0
    for i, car := range cars {
        car.Km += 50
        if i+1 < len(cars) {
            cars[i+1].Km = 0
        }
        total += car.Km
    }
    fmt.Println(total, cars[2].Km)
}

What does the program print?

Reveal the reasoning

750 0. cars is an array, so range reads a copy of all three cars, made before the first pass. Setting cars[i+1].Km to 0 changes the array, but the next car still comes from the copy, with 200 and then 300 km. car.Km += 50 changes only the copy, so the passes add 150, 250, and 350. cars[2].Km is 0, because the second pass set it in the array itself.

Practice tickets

  1. GO-401 · A minibus leaves with more pupils than seats → Free
  2. GO-402 · Destination signs cut names one letter early → Sign in · Pro
  3. GO-403 · The departure screen leaves out the next flights → Sign in · Pro

Keep exploring

Function calls →

Primary references