notexOPENOverview
Notex/Languages/The versatile for-range Statement
02.10LESSON 10 OF 19 · 9 MIN READ

The versatile for-range Statement

Always free to read

The fourth for statement format is called a for-range loop and resembles the iterators found in other languages. It is for iterating over elements in some of Go’s built-in types. This section shows how to use a for-range loop with integers, strings, arrays, slices, maps, and functions. When I cover channels in the related discussion>, I will talk about how to use them with for-range loops.

Note

Use a for-range loop to iterate over the built-in compound types and user-defined types that are based on them.

The simplest for-range loop is the integer loop. The code in Example 3-13 functions identically to Example 3-8, assigning 0 to 9, inclusive, to i:

Example 3-13. An integer for-range loop

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

There are a few details to note about ranging over integers. If the range limit is less than or equal to zero, the loop is skipped. Also, if the counter is not used inside the for loop, do not include it:

for range 10 {
    fmt.Println("printing this message 10 times")
}

Note

Why does Go have a shortcut for looping through integers? In 2023, Russ Cox, the former tech lead for Go, noted that “In the Go code I have examined from the main repo, approximately half of existing 3-clause for loops can be converted to range over integer.” Since this was such a frequent pattern, Go 1.22 added a simple way to express it.

Next, let’s take a look at using a for-range loop with a slice. You can try out the code in Example 3-14 on The Go Playground.

Example 3-14. Using for-range to loop over a slice

evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
    fmt.Println(i, v)
}

Running this code produces the following output:

0 2
1 4
2 6
3 8
4 10
5 12

Iterating over a map is similar to iterating over a slice:

ages := map[string]int{"Moses": 120, "David": 70, "Abraham": 175}
for k, v := range ages {
    fmt.Println(k, v)
}

What makes a for-range loop for slices or maps interesting is that you get two loop variables. The first variable is the position in the data structure being iterated, while the second is the value at that position. The idiomatic names for the two loop variables depend on what is being looped over. When looping over an array, slice, or string, an i for index is commonly used. When iterating through a map, k (for key) is used instead.

The second variable is frequently called v for value, but is sometimes named based on the type of the values being iterated. Of course, you can give the variables any names that you like. If the body of the loop contains only a few statements, single-letter variable names work well. For longer (or nested) loops, you’ll want to use more descriptive names.

What if you don’t use the first variable within your for-range loop? Remember, Go requires you to access all declared variables, and this rule applies to the ones declared as part of a for loop too. If you don’t access the key, use an underscore (_) as the variable’s name. This tells Go to ignore the first variable.

Let’s rewrite the slice ranging code so it doesn’t print out the position. You can run the code in Example 3-15 on The Go Playground.

Example 3-15. Ignoring the slice index in a for-range loop

evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    fmt.Println(v)
}

Running this code produces the following output:

2
4
6
8
10
12

Tip

Anytime you are in a situation where a value is returned, but you want to ignore it, use an underscore to hide the value. You’ll see the underscore pattern again when I talk about functions in the related discussion and packages in the related discussion.

What if you want the key but don’t want the value? In this situation, Go allows you to just leave off the second variable. This is valid Go code:

uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true}
for k := range uniqueNames {
    fmt.Println(k)
}

The most common reason for iterating over the key is when a map is being used as a set. In those situations, the value is unimportant. However, you can also leave off the value when iterating over arrays or slices. This is rare, as the usual reason for iterating over a linear data structure is to access the data. If you find yourself using this format for an array or slice, there’s an excellent chance that you have chosen the wrong data structure and should consider refactoring.

Note

When you look at channels in the related discussion, you’ll see a situation where a for-range loop returns only a single value each time the loop iterates.

Map iteration order

There’s something interesting about how a for-range loop iterates over a map. You can run the code in Example 3-16 on The Go Playground.

Example 3-16. Map iteration order varies

m := map[string]int{
    "a": 1,
    "c": 3,
    "b": 2,
}

for i := range 3 {
    fmt.Println("Loop", i)
    for k, v := range m {
        fmt.Println(k, v)
    }
}

When you build and run this program, the output varies. Here is one possibility:

Loop 0
c 3
b 2
a 1
Loop 1
a 1
c 3
b 2
Loop 2
b 2
a 1
c 3

The order of the keys and values varies; some runs may be identical. This is actually a security feature. In earlier Go versions, the iteration order for keys in a map was usually (but not always) the same if you inserted the same items into a map. This caused two problems:

  • People would write code that assumed that the order was fixed, and this code would break at weird times.

  • If maps always hash items to the exact same values and you know that a server is storing user data in a map, you can slow down that server with an attack called a Hash DoS by sending it specially crafted data with keys that all hash to the same bucket.

To prevent both of these problems, the Go team made two changes to the map implementation. First, they modified the hash algorithm for maps to include a random number that’s generated every time a map variable is created. Next, they made the order of a for-range iteration over a map vary a bit each time the map is looped over. These two changes make it far harder to implement a Hash DoS attack.

Note

This rule has one exception. To make it easier to debug and log maps, the formatting functions (like fmt.Println) always output maps with their keys in ascending sorted order.

The for-range value is a copy

You should be aware that each time a for-range loop iterates over a compound type, it copies the value from the compound type to the value variable. Modifying the value variable will not modify the value in the compound type. Example 3-17 shows a quick program to demonstrate this. You can try it out on The Go Playground.

Example 3-17. Modifying the value doesn’t modify the source

evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
    v *= 2
}
fmt.Println(evenVals)

Running this code gives the following output:

[2 4 6 8 10 12]

Iterating over strings

As I mentioned earlier, you can also use a string with a for-range loop. Let’s take a look. You can run the code in Example 3-18 on your computer or on The Go Playground.

Example 3-18. Iterating over strings

samples := []string{"hello", "apple_π!"}
for _, sample := range samples {
    for i, r := range sample {
        fmt.Println(i, r, string(r))
    }
    fmt.Println()
}

The output when the code iterates over the word “hello” has no surprises:

0 104 h
1 101 e
2 108 l
3 108 l
4 111 o

In the first column is the index; in the second, the numeric value of the letter; and in the third is the numeric value of the letter type converted to a string.

Looking at the result for “apple_π!” is more interesting:

0 97 a
1 112 p
2 112 p
3 108 l
4 101 e
5 95 _
6 960 π
8 33 !

Two things to notice about this output: First, the first column skips the number 7. Second, the value at position 6 is 960. That’s far larger than what can fit in a byte. But in the related discussion, you saw that strings were made out of bytes. What’s going on?

What you are seeing is special behavior from iterating over a string with a for-range loop. It iterates over the runes, not the bytes. Whenever a for-range loop encounters a multibyte rune in a string, it converts the UTF-8 representation into a single 32-bit number and assigns it to the value. The offset is incremented by the number of bytes in the rune. If the for-range loop encounters a byte that doesn’t represent a valid UTF-8 value, the Unicode replacement character (hex value 0xfffd) is returned instead.

Tip

Use a for-range loop to access the runes in a string in order. The first variable holds the number of bytes from the beginning of the string, but the type of the second variable is rune.

Iterating over functions

The for-range style is so convenient that developers wanted to use it to iterate over anything. Go 1.23 addressed this by enhancing the for-range loop to support iterating over functions. The function provides values and indicates when iteration should stop.

Rather that constructing iterator functions directly, the idiomatic pattern is for a builder function to take in a value and return an iterator functions for that value. If you aren’t familiar with functions that return functions, I talk about them in “Returning Functions from Functions”. Creating your own iterator builder functions is detailed in the related discussion (you have to learn about closures and generics first), but the standard library includes several iterator builder functions for common use cases.

Passing a slice to the slices.Values function returns a function that provides the values in a slice without the index:

mySuperBestFriends := []string{"Wendy", "Marvin", "Aquaman"}
for v := range slices.Values(mySuperBestFriends) {
    fmt.Println(v, "is one of my best friends!")
}

There’s also slices.Backward for iterating through a slice in reverse order:

for i, v := range slices.Backward(mySuperBestFriends) {
    fmt.Println("number", i, "friend is", v)
}

Another interesting function is slices.Chunk, which returns a function that creates subslices out of a parent slice:

valsToPair := []int{1,2,2,4,3,6,4,8,5,10}
pairs := map[int]int{}
for sub := range slices.Chunk(valsToPair, 2) {
    pairs[sub[0]] = sub[1]
}

The maps.Keys function returns a function that provides the keys of a map, while maps.Values does the same for map values:

for k := range maps.Keys(pairs) {
    fmt.Println(k)
}

for v := range maps.Values(pairs) {
    fmt.Println(v)
}

The strings package includes several functions for iterating through parts of a string. They include strings.Lines (which returns newline-delimited substrings), strings.FieldsSeq (for splitting on one or more whitespace characters), and strings.SplitSeq (for splitting on a provided separator). You can try out all of these iterator examples on The Go Playground or find them in the examples/iter directory.

Tip

The bytes package provides string iterator analogues that work with byte slices.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K