Now that I’ve covered all forms of the for statement, you might be wondering when to use which format. Most of the time, you should use the for-range format. A for-range loop is the best way to iterate up to an integer. It’s also the best way to walk through a string, since it properly gives you back runes instead of bytes. You have also seen that a for-range loop works well for iterating through slices and maps. Functional iterators make map and slice iteration more flexible and extend the idiom to user-defined types. You’ll see in the related discussion that channels work naturally with for-range as well.
Tip
Favor the for-range loop. For common cases, it avoids a great deal of the boilerplate code required for the other for loop styles.
When should you use the complete for loop? It’s most useful when there’s a nontrivial increment or test. While you might be able to use some combination of if, continue, and break within a for-range loop, a standard for loop’s test and increment clauses provide a clear way to indicate when iteration ends and how many elements are being skipped. For example, here’s code that counts the number of runes in a slice of bytes:
b := []byte{0xf0, 0x9f, 0x91, 0x8b, 0x2c, 0x20, 0xf0, 0x9f, 0x8c, 0x8e, 0x21}
count := 0
width := 0
for i := 0; i < len(b); i += width {
_, width = utf8.DecodeRune(b[i:])
count++
}
fmt.Println(count)
The size of the increment depends on the value of the data, which cannot work with a simple for-range loop over a slice.
The condition-only for loop is, like the while loop it replaces, useful when you are looping based on a calculated value.
The infinite for loop is useful in some situations, but you need to use it carefully. The body of the for loop should always contain a break or return, because you’ll rarely want to loop forever. Real-world programs should bound iteration and fail gracefully when they can’t complete operations. As you’ve seen, an infinite for loop can be combined with an if statement to simulate the do statements present in other languages. An infinite for loop is used to implement some versions of the iterator pattern, which you will look at when I review the standard library in the related discussion. It’s also used in the for-select loop concurrency pattern, which is covered in the related discussion.
The lifespan of a for-loop variable
Variables declared in the body of a for loop are recreated on each iteration. What might be surprising is that the variables declared in the for loop’s initialization section are also recreated on each iteration. For example, when you write the following code, the loop creates a new variable named i 10 times:
for i := range 10 {
fmt.Println("I'm a new i:", i)
}
In most situations, this behavior is invisible; it looks like the same instance is being reused and assigned a new value. In versions of Go before 1.22, the initialization section variables were created once and reused on each iteration through the for loop. That behavior was changed because reusing the variables resulted in subtle bugs when they were referenced within closures declared in the for loop body. (I discuss closures in detail in “Closures”.)
Even though this change eliminated a potential bug, it was a backward-breaking change. You can control whether to enable it by specifying the Go version in the go directive in the go.mod file for your module. I talk about this in greater detail in the related discussion.