Today I learned that using range in Go to iterate over slices can lead to a rather hard-to-detect logical error. And I’m talking about operating on copies of slice elements instead of the original.
Let’s take a simple structure with one method:
type Person struct {
Name string
Age int
}
func (person *Person) Birthday() {
person.Age += 1
}
The pointer receiver here makes sure that Birthday() method
operates on the original object. At least that’s what I thought.
Create a slice of several people:
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
Let’s print this object:
fmt.Println(people)
And now i want to increase the age of each person by 1, so I run the Birthday() on each element of slice using range:
for _, person := range people {
person.Birthday()
}
fmt.Println(people)
Simple, huh?
But if I check the age value of each person - nothing happened.
[{Alice 30} {Bob 25} {Charlie 35}]
[{Alice 30} {Bob 25} {Charlie 35}]
Here comes the range and it’s magic.
I was expecting that usage of Brithday() method always operates on original object, since it uses a pointer receiver.
And it’s like this.
Problem is in the construction:
for _, person := range people {
person is the copy of slice people element, not the reference to the original one. To make it work properly, this must be changed to using the slice index, like this:
for i := range people {
people[i].Birthday()
}
We can even check the pointers in both cases:
fmt.Println(people)
for _, person := range people {
person.Birthday()
fmt.Printf("%p\n", &person)
}
fmt.Println(people)
for i := range people {
people[i].Birthday()
fmt.Printf("%p\n", &people[i])
}
fmt.Println(people)
Returns:
[{Alice 30} {Bob 25} {Charlie 35}]
0xb7c0b422090
0xb7c0b4220a8
0xb7c0b4220c0
[{Alice 30} {Bob 25} {Charlie 35}]
0xb7c0b428000
0xb7c0b428018
0xb7c0b428030
[{Alice 31} {Bob 26} {Charlie 36}]
Addresses are different :)
Conclusion?
Always use indexes when operating on original objects.
Example’s source code available on GitHub