TIL: Kopiowanie obiektów w pętli range w Go
TIL Golang

TIL: Kopiowanie obiektów w pętli range w Go

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