Modifying map while iterating over it in Go

Viewed 2791

Given the following code I would expected an infinite loop but the loop is being stopped at certain point.

m := make(map[int]string, 4)
m[0] = "Foo"
    
for k, v := range m {
    m[k+1] = v
}

I cannot figure out what happen under the hood because different execution return different output. For example these are a few outputs from different executions:

map[0:Foo 1:Foo 2:Foo 3:Foo 4:Foo 5:Foo 6:Foo 7:Foo]
map[0:Foo 1:Foo]
map[0:Foo 1:Foo 2:Foo]

How range works in order to exit from loop at certain point and what is the exit condition?

3 Answers

Spec: For statements with range clause says the behavior is unpredictable:

The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0.

Adding elements to the map you're ranging over, those entries may or may not be visited by the loop, you should not assume anything regarding to that.

Based on the language spec:

If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped.

So if the new elements are skipped, the for-loop eventually ends.

The other answers have already explained the behavior you observe with your snippet.

Because your title is rather generic but your snippet only covers the addition of map entries while iterating over the map, here is a complementary example that should convince you that "cross-removing" map entries while iterating over the map is a bad idea (Playground):

package main

import "fmt"

func main() {
    m := map[string]int{"foo": 0, "bar": 1, "baz": 2}
    for k := range m {
        if k == "foo" {
            delete(m, "bar")
        }
        if k == "bar" {
            delete(m, "foo")
        }
    }
    fmt.Println(m)
}

The spec says:

The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced.

As a result, the program outputs either map[bar:1 baz:2] or map[baz:2 foo:0], but there is no way to tell which.

Related