Dzisiaj nauczyłem się, że użycie range w Go do iterowania po slice’ach
może prowadzić do dosyć ciężkiego do wykrycia błędu logicznego. A mówię
oczywiście o operowaniu na kopiach elementu slice’a zamiast oryginału.
Sytuacja - weźmy prostą strukturę, z jedną metodą:
type Person struct {
Name string
Age int
}
func (person *Person) Birthday() {
person.Age += 1
}
Receiver wskaźnikowy w tym przypadku sprawia, że metoda Birthday() operuje na oryginalnym obiekcie. Przynajmniej tak mi się wydawało.
I zróbmy slice kilku osób:
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
Wyprintujmy sobie ten obiekt:
fmt.Println(people)
Teraz chcemy zwiększyć wiek każdej osoby o 1, więc wykonuję pętlą funkcję
Birthday() na każdym elemencie slice’a:
for _, person := range people {
person.Birthday()
}
Wygląda prosto i fajnie, prawda? Tylko jeżeli teraz sprawdzę wiek każdej osoby, to wyjdzie, że nic się nie zmieniło:
[{Alice 30} {Bob 25} {Charlie 35}]
[{Alice 30} {Bob 25} {Charlie 35}]
I tu właśnie pojawia się range i jego magia. Skoro metoa Birthday() przyjmuje
wskaźnik do struktury, to powinna operować na oryginalnym obiekcie a nie jego
kopii.
I tak właśnie jest.
Problem polega na konstrukcji:
for _, person := range people {
person jest tutaj kopią elementu slice’a people, a nie referencją do niego.
Żeby metoda zadziałała poprawnie, zamiast iteracji po obiektach, należy
iterować po indeksach slice’a:
for i := range people {
people[i].Birthday()
}
Można by nawet sprawdzić wskaźniki do obydwu przypadkach:
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)
Zwrotka:
[{Alice 30} {Bob 25} {Charlie 35}]
0xb7c0b422090
0xb7c0b4220a8
0xb7c0b4220c0
[{Alice 30} {Bob 25} {Charlie 35}]
0xb7c0b428000
0xb7c0b428018
0xb7c0b428030
[{Alice 31} {Bob 26} {Charlie 36}]
Adresy się różnią :)
Wniosek?
Zawsze używać indeksu, jeżeli chcemy operować na oryginalnych wartościach slice’a.
Kompletny kod z tego przykładu dostępny na GitHubie