“for” statement and its all faces in Golang

Michał Łowicki
Jan 15 · 9 min read

In contrast to many other languages, Go doesn’t have multiple constructs for executing block of code repeatedly. There’s single statement (for statement) which takes different forms to support various scenarios and also integrates well with Go-specific mechanisms like slices or channels. In this post we’ll examine under a microscope something which is more interesting than it may seem at first glance.

1. Single boolean condition

Let’s start with the simplest form where only optional bool condition is in use (source code):

var v int = 39
for v != 0 {
fmt.Println(v)
if v > 16 {
v /= 4
} else {
v /= 2
}
}

Output:

39
9
4
2
1

It’s pretty straightforward so the result shouldn’t be any surprise. Delicate modification unveils rule which must be followed by conditions used inside for statements (source code):

var v int = 39
for v {
fmt.Println(v)
if v > 16 {
v /= 4
} else {
v /= 2
}
}

Such program doesn’t compile. It’s required to use boolean condition:

non-bool v (type int) used as for condition

Condition is checked before each loop so if it evaluates to false immediately, block isn’t executed even once.

It’s legal to omit condition completely and then it’s equivalent to boolean value true:

package mainimport (
"net/http"
"time"
)
func main() {
for {
resp, err := http.Get("https://facebook.com")
if err != nil {
time.Sleep(time.Second)
}
defer resp.Body.Close()
break
}
}

2. “For” clause

This is the form, most coders are familiar with. It’s an extension to previous one. It adds init and post statements (source code):

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

Output:

0
1
2
3
4

Init statement ( i := 0) is executed only once before evaluating condition for the very first time. We can omit this statement if needed (source code):

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

Output:

0
1
2
3
4

Semicolon before condition is required. What can be used as init statement?

ch := make(chan int, 1)
i := 0
for ch <- 1; i < 5; i++ {
fmt.Println(i)
}
i := 0
for fmt.Println("start"); i < 5; i++ {
fmt.Println(i)
}

Post statement is executed each time block inside for statement is executed (source code):

for i := 0; i < 5; fmt.Println("after") {
fmt.Println(i)
i++
}

Output:

0
after
1
after
2
after
3
after
4
after
for i := 0; i < 5; fmt.Println("after") {
i++
break
}

This program doesn’t print anything (source code) because of break statement but it’s different story when continue statement is in use (source code):

for i := 0; i < 5; fmt.Println("after") {
i++
continue
}

Output:

after
after
after
after
after

The same rules apply to post statement but with one exception — short variable declaration is not possible there (source code):

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

Such code doesn’t compile and gives:

syntax error: cannot declare in post statement of for loop

Post statement can be omitted but semicolon after condition is then required (source code):

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

Output:

0
1
2
3
4

3. “Range” clause

This variant allows to iterate through array, pointer to array, slice, string, map or values received on a channel. Let’s go through all types to observe all nuances.

slice

Basic use cases are pretty obvious (source code):

nums := []string{"one", "two", "three"}
for idx, num := range nums {
fmt.Printf("%d: %s\n", idx, num)
}
for idx := range nums {
fmt.Println(idx)
}
for range nums {
fmt.Println("tick")
}

Output:

0: one
1: two
2: three
0
1
2
tick
tick
tick

It’s worth to note that assigning different slice to a variable won’t change iterations (source code):

a := []string{"one", "two", "three", "four", "five"}
b := a[:3]
for idx, num := range b {
b = b[:5]
fmt.Printf("%d: %s\n", idx, num)
}

Output:

0: one
1: two
2: three

It’s possible though to change values of slice’s element during iterations and this will be visible if respective iteration entry hasn’t been created yet (source code):

a := []string{"one", "two", "three", "four", "five"}
for idx, num := range a {
a[4] = "six"
fmt.Printf("%d: %s\n", idx, num)
}

Output:

0: one
1: two
2: three
3: four
4: six

No iterations will be created for nil slice.

array

Usage and behaviour is similar to slice (source code):

nums := [...]string{"one", "two", "three"}
for idx, num := range nums {
fmt.Printf("%d: %s\n", idx, num)
}
for idx := range nums {
fmt.Println(idx)
}
for range nums {
fmt.Println("tick")
}

Output:

0: one
1: two
2: three
0
1
2
tick
tick
tick

Changing the content of array won’t be reflected when for loop already started (source code):

a := [...]string{"one", "two", "three", "four", "five"}
for idx, num := range a {
a[4] = "six"
fmt.Printf("%d: %s\n", idx, num)
}

Output:

0: one
1: two
2: three
3: four
4: five

It’ll be reflected though if at most one iteration variable will be used (source code):

a := [...]string{"one", "two", "three", "four", "five"}
for idx := range a {
a[4] = "six"
fmt.Printf("%d: %s\n", idx, a[idx])
}

Output:

0: one
1: two
2: three
3: four
4: six

pointer to array

This is almost identical to pure array (source code):

nums := &[...]string{"one", "two", "three"}
for idx, num := range *nums {
fmt.Printf("%d: %s\n", idx, num)
}
for idx, num := range nums {
fmt.Printf("%d: %s\n", idx, num)
}
for idx := range nums {
fmt.Println(idx)
}
for range nums {
fmt.Println("tick")
}

Output:

0: one
1: two
2: three
0: one
1: two
2: three
0
1
2
tick
tick
tick

The difference is with changing value of array’s elements which will be visible in loop which has already started even if two iteration variables will be used (source code):

a := &[...]string{"one", "two", "three", "four", "five"}
for idx, num := range a {
a[4] = "six"
fmt.Printf("%d: %s\n", idx, num)
}

Output:

0: one
1: two
2: three
3: four
4: six

string

We’ll explore this scenario with string containing non-ASCII characters (source code):

s := "żądło"
for i := 0; i < len(s); i++ {
fmt.Printf("%q\n", s[i])
}
fmt.Println(strings.Repeat("*", 10))
for i, c := range s {
fmt.Printf("%d: %q\n", i, c)
}

Output:

'Å'
'¼'
'Ä'
'\u0085'
'd'
'Å'
'\u0082'
'o'
**********
0: 'ż'
2: 'ą'
4: 'd'
5: 'ł'
7: 'o'

How to interpret above result? Using built-in len function, loop iterates over each byte within a string. The second for statement goes through Unicode code points, encoded as UTF-8. Index value points to index of first byte of the current code point.

If only one iteration variable is present then it’ll be assigned to index iteration value — index of first byte of current code point (source code):

s := "żądło"
for i := range s {
fmt.Printf("%d\n", i)
}

Output:

0
2
4
5
7

map

Let’s begin with few basic examples (source code):

m := map[string]int{"one": 1, "two": 2, "three": 3}
for range m {
fmt.Println("loop")
}
for k := range m {
fmt.Printf("key: %q\n", k)
}
for k, v := range m {
fmt.Printf("key: %q, value: %d\n", k, v)
}

Output:

loop
loop
loop
key: "one"
key: "two"
key: "three"
key: "one", value: 1
key: "two", value: 2
key: "three", value: 3

Idiomatic code skips iteration variable instead of using blank identifier (source code):

m := map[string]int{"one": 1, "two": 2, "three": 3}
for range m { // idiomatic
fmt.Println("a")
}
for _ = range m {
fmt.Println("b")
}
for _, _ = range m {
fmt.Println("c")
}
for k := range m { // idiomatic
fmt.Printf("key: %q\n", k)
}
for k, _ := range m {
fmt.Printf("key: %q\n", k)
}

Output:

a
a
a
b
b
b
c
c
c
key: "two"
key: "three"
key: "one"
key: "one"
key: "two"
key: "three"

There’re none iterations over nil map (source code):

var m map[string]int
for range m {
fmt.Println("foo")
}
m = map[string]int{"one": 1, "two": 2}
for range m {
fmt.Println("bar")
}

Output:

bar
bar

Map is a mutable data structure so while iterating, its content may change by adding / removing keys or changing values associated with keys. If key has been added while going over map then specification says it’s undefined behaviour if iteration for that key will be created — will be either skipped or processed (source code):

m := make(map[string]int)
m["one"] = 1
m["two"] = 2
for k := range m {
fmt.Println(k)
m["four"] = 4
}

Such program will always print one and two (order is not defined). It’s undefined though if also four will be written to stdout.

It’s guaranteed by spec that if key for which iteration value hasn’t been produced yet will be removed then such key won’t be produced (source code):

m := make(map[string]int)
m["one"] = 1
m["two"] = 2
for k := range m {
fmt.Println(k)
delete(m, "two")
}

If the first iteration value will be “one” then it’s guaranteed that second iteration won’t happen.

Changes to map’s values will be reflected in already running loop (source code):

nums := map[string]int{"one": 1, "two": 2}
for idx, num := range nums {
fmt.Printf("%s: %d\n", idx, num)
nums["one"] = 11
nums["two"] = 22
}
nums = map[string]int{"one": 1, "two": 2}
for idx := range nums {
fmt.Printf("%s: %d\n", idx, nums[idx])
nums["one"] = 11
nums["two"] = 22
}

Output depends on the order of how keys will be processed but in any way, the second iteration will show updated value like:

one: 1
two: 22
two: 2
one: 11

channel

Range clause with channel iterates over all values sent to channel until that channel is closed (source code):

ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 1
time.Sleep(2 * time.Second)
ch <- 2
time.Sleep(time.Second)
close(ch)
}()
fmt.Printf("before:\t\t%s\n", time.Now())
for v := range ch {
fmt.Printf("tick #%d:\t%s\n", v, time.Now())
}
fmt.Printf("after:\t\t%s\n", time.Now())

Output:

before:		2009-11-10 23:00:00 +0000 UTC m=+0.000000001
tick #1: 2009-11-10 23:00:03 +0000 UTC m=+3.000000001
tick #2: 2009-11-10 23:00:05 +0000 UTC m=+5.000000001
after: 2009-11-10 23:00:06 +0000 UTC m=+6.000000001

If nothing has been sent for already closed channel then no iterations will be executed (source code):

ch := make(chan int)
close(ch)
for range ch {
fmt.Println("tick")
}
fmt.Println("done!")

It printsdone! to stdout.

Range clause with channel supports only one (optional) iteration variable (source code):

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for a, b := range ch {
}

This program gives compile-time error: ./prog.go:8:14: too many variables in range.

If channel is not closed (source code):

ch := make(chan int)
for range ch {
}

or nil channel is used (zero value for channel types) (source code):

var ch chan int
for range ch {
}

then for statement hangs forever and ultimately produces runtime error:

fatal error: all goroutines are asleep - deadlock!

short variable declaration

Variable defined in outer scope will be still reachable after for statement and its value will be set to value from last iteration (source code):

nums := []string{"zero", "one", "two"}
var idx int
for idx = range nums {
}
fmt.Println(idx)

Output is 2.

If variable within range clause will be declared using short variable declaration then its scope will be for statement (source code):

nums := []string{"zero", "one", "two"}
for idx := range nums {
}
fmt.Println(idx)

This produce compile-time error undefined: idx.

range clause and the address of iteration variable

This topic has been described in one of previous stories so let’s link it here. It’s recommended to read it since it’s a trap many engineers fall into.

Resources


👏👏👏 below to help others discover this story. Please follow me here or on Twitter if you want to get updates about new posts or boost work on future stories.

golangspec

A series dedicated to deeply understand Go’s specification and language’s nuances

Michał Łowicki

Written by

Software engineering manager at Facebook, previously Opera, never satisfied.

golangspec

A series dedicated to deeply understand Go’s specification and language’s nuances

More From Medium

More on Programming from golangspec

More on Programming from golangspec

Defer statement in Golang (part I)

23

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade