Когда у вас есть фрагмент сложных значений, как вы их обновляете? В этой статье обсуждаются 3 решения. Последний основан на разыменовании указателя, который сочетает в себе лучшее из двух других, но без их недостатков.

Проблема

Давайте возьмем пример простой структуры Person и предположим, что у нас есть срез из 2 элементов:

type Person struct {
   Name     string
   Children []Child
}
persons := []Person{
  {
    Name: "John",
    Children: []Child{
      {
        Name: "John Jr",
      },
    },
  },
  {
    Name: "Jack",
  },
}

Теперь предположим, что мы хотим изменить Name первого лица:

p := persons[0]
p.Name = "Joe"
fmt.Printf("%s", persons[0].Name) // still displays `John`

Хм… 🤨

На самом деле в приведенном выше коде произошло то, что новая переменнаяpбыла инициализирована копией содержимого в первой позиции фрагментаpersons. > Это означает, что изменениеNameвpне повлияло на содержаниеpersons.

Решение №1: обновление среза после изменения

Самый простой способ решить проблему — сбросить запись слайса с только что обновленной переменной:

p := persons[0]
p.Name = "Joe"
persons[0] = p
fmt.Printf("%s", persons[0].Name) // displays `Joe`, now

Это работает, но этот дополнительный оператор persons[0] = p кажется немного нелогичным или неожиданным, не так ли? И становится еще хуже, когда мы пытаемся обновить один из дочерних Name:

p := persons[0]
p.Name = "Joe"
c := persons[0].Children[0]
c.Name = "Joe Jr"
p.Children[0] = c
persons[0] = p
fmt.Printf("%s", persons[0]) // displays `Joe` and `Joe Jr`

Здесь мы должны дважды скопировать переменную обратно в срез (т. е. один раз на уровень). Это несколько подверженный ошибкам и не очень масштабируемый способ работы.

Решение №2: непосредственное обновление элемента slice

Второе решение заключается в прямом доступе к элементу в слайсе persons при изменении его поля Name:

persons[0].Name = "Jimmy"
fmt.Printf("%s", persons[0].Name) // displays `Jimmy`

Это тоже работает, даже если код немного сложнее читать, если было несколько изменений (но это мое личное мнение).

Однако обратите внимание, что это решение также работает для обновления вложенных элементов:

persons[0].Children[0].Name = "Jimmy Jr"

Решение №3: разыменование указателя на элемент slice

Наличие переменной — это хорошо, но вместо копии элемента slice она может быть указателем на этот элемент. При использовании оператора * для «разыменования» указателя значение, на которое указывал указатель (т. е. запись в срезе), немедленно обновляется:

pr := &persons[0]
(*pr).Name = "Jeff"
fmt.Printf("first parent: %s\n", persons[0].Name) // displays `Jeff`

Как и следовало ожидать, это решение работает и для вложенных элементов:

cr := &persons[0].Children[0]
(*cr).Name = "Jeff Jr"
fmt.Printf("%s\n", persons[0].Children[0]) // displays `Jeff Jr`

Разыменование указателя — это удобное решение, которое дает преимущество наличия переменной, но при этом не нужно иметь дело с последующим «копированием» изменений в резервном фрагменте.

Не стесняйтесь попробовать эти решения самостоятельно на игровой площадке.