unexpected-go

Unexpected Golang behaviors

View on GitHub

Goroutines in loops may not behave as expected before Go 1.22

Update: this has been fixed in Go 1.22 and the following contents is left for historical reasons.

This was actually documented in CommonMistakes wiki page of the official golang’s GitHub repository (notice, this is the only common mistake documented there as this page is being written) but since its visibility is not good enough, it’s always a good start.

Code

Consider the following code:

package main

import "fmt"

func main() {
	ch := make(chan int)
	slice := []int{0, 1}

	for _, v := range slice {
		go func() { ch <- v }()
	}

	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

One may expect this to print:

0
1

But actually this code is not deterministic, and in most cases will just print

1
1

Why?

When iterating using i, v := range whatever, the i and v variables are defined only once for the scope, and then their values are overwritten. Since the goroutines reference the same variable all the time, but they are probably executed after the for loop is finished, they all access the same last value of it: 1.

You can check this by printing the pointer address, which never changes:

package main

import "fmt"

func main() {
	for _, v := range []int{0, 1} {
	    fmt.Printf("%p\n", &v)
	}
}

Related

for ;;

This can be also unexpected if we do:

package main

import "fmt"

func main() {
	ch := make(chan int)

	for i := 0; i < 2; i++ {
		go func() { ch <- i }()
	}

	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

Where the output is:

2
2

Although our goroutines should have never been executed with i >= 2 since i < 2 is the stop condition for the loop.

Receiver call

As a corolary, but still worth mentioning, this also applies to receiver functions, as a simple example:

package main

import (
	"fmt"
	"sync"
)

type S string

func (s S) Print() {
	fmt.Println(s)
}

func main() {
	slice := []S{"a", "b"}

	wg := sync.WaitGroup{}
	wg.Add(2)

	for _, s := range slice {
		go func() {
			s.Print()
			wg.Done()
		}()
	}

	wg.Wait()
}

Prints:

b
b