Tomas Vik

Go: range is like a function call

When I watched Ultimate Go Programming today, I learned about an unintuitive Go programming language behaviour. I’m going to show you.

Have a look at the following code and tell me what will be printed in the console:

arr := [...]string{"Hello", "you", "handsome", "person"}
fmt.Printf("before: %q\n", arr[1])
for i, v := range arr {
  // at the very first iteration we change second word: you -> from
  arr[1] = "from"
  if i == 1 {
    // second iteration, we print the value of second word from range
    fmt.Printf("after: %q\n", v)
  }
}

In the first iteration, we changed "you" to "from"; in the second iteration we printed the second element from the array. You might expect that you’ll see:

before: "you"
after: "from"

Try it for yourself.

The result is

before: "you"
after: "you"

Now let’s change the array to a slice:

slice := []string{"Hello", "you", "handsome", "person"}
fmt.Printf("before: %q\n", slice[1])
for i, v := range slice {
  slice[1] = "from"
  if i == 1 {
    fmt.Printf("after: %q\n", v)
  }
}

When you run this code (playground), you’ll see the expected

before: "you"
after: "from"

Why do you think this is? (answer in 3.. 2.. 1..)

The answer

The range clause works exactly like a function call in the sense that you pass arguments to it by value. That means that when you write range thing, the Go runtime will make a copy of the thing and ranges over the copy.

We saw that ranging over slices behaves as we expect. That’s because of the internal representation of a slice:

slice

When the Go runtime copies the slice, it only copies the small data structure containing a pointer to the backing array, length and capacity. That’s why we can see changes made to the slice during iteration, both the original slice and the copied value used in range point to the same backing array.

Now you might start to see why the array from the first example behaved differently.

array

When range copies the arr value, it copies the whole array. Changing the original array doesn’t affect the range iterations.

So there you have it. **range is like a function call. The language specification doesn’t state that explicitly, but luckily, you probably won’t see this issue during normal programming. You can range over slices, maps, channels and arrays, but only arrays are copied whole. I hope this behaviour surprised you as it surprised me.