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"
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:
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.
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.