Что такое глубокое копирование?

Глубокая копия — это когда мы копируем объект, а новый и старый объекты не используют общую память.

что это значит?

давайте начнем с примера, чтобы увидеть, каков вариант использования.

В этом примере мы будем иметь дело с серверной частью социальных сетей.

Структура, с которой мы будем работать, — это User, которая также будет содержать рекурсивный список друзей, а фамилия необязательна, пустая строка — допустимая фамилия (мы предполагаем, что программист не читал мою статью об использовании Option вместо использования указатель)

https://medium.com/@aryehlevklein/golang-generics-101-and-creating-an-option-type-8a8cafdf407a

type User struct {
 Name     string
 LastName *string
 Id       string
 Friends  []User
}

Теперь давайте создадим нашего первого пользователя с одним другом:

myLast := "lev"
user1 := User{
   Name:     "aryeh",
   LastName: &myLast,
   Id:       "0",
   Friends:  []User{{
      Name:     "john",
      LastName: nil,
      Id:       "1",
      Friends:  nil,
   }},
}

Теперь мы получаем запрос на дублирование пользователя и просто меняем фамилию и меняем друга.

легко, правда?

скопируйте пользователя:

user2 := user1

Теперь мы закончили копирование и можем использовать user2.

if user2.LastName != nil {
   *user2.LastName = "notlev"
}

if len(user2.Friends) > 0 {
   user2.Friends[0] = User{
      Name:     "notjoe",
      LastName: nil,
      Id:       "",
      Friends:  nil,
   }
}

(Я специально не делал новый указатель на фамилию, чтобы показать свою точку зрения)

Теперь у нас есть два разных пользователя, кусок пирога!

но не совсем так, если мы напечатаем друга и фамилию пользователя user1, которые мы не собирались менять, мы получим

fmt.Println(*user1.LastName)
fmt.Println(user1.Friends)

нотлев

[{notjoe ‹nil› []}]

Итак, что случилось?

когда мы устанавливаем

user2 := user1

он сделал неглубокую копию user1, что означает, что он скопировал LastName и Friends, как они есть в user1, которые являются указателями на места в памяти, а не на сами значения (срезы являются указателями).

Таким образом, user1.LastName и user2.LastName будут указывать на одну и ту же память, и любое изменение, которое мы делаем со значениями в одном из них, изменит другое (то же самое касается друзей).

Как решить эту проблему? мы делаем глубокую копию, при которой мы копируем значения в памяти, а не указатели на значения.

так что это будет означать, что для LastName будет создан новый указатель с тем же значением, а для друзей будет создан новый фрагмент с теми же значениями.

(хотя в этом случае мы можем создать его с другими значениями в копии, поскольку мы уже знаем, чего хотим, но в большинстве случаев это не так)

как мы это решим?

решение 1:

используйте пакет Reflect, это должно подойти, если производительность не критична для вашего приложения, а решение очень хорошо протестировано ивсе поля экспортируются.

решение 2:

используйте json.Marshal, а затем json.Unmarshal следующим образом:

var user2 User

b, _ := json.Marshal(user1)

json.Unmarshal(b, &user2)

это достойное решение, но только для экспортируемых полей, а также будет иметь высокие затраты на производительность.

решение 3:

Вручную скопируйте каждую часть, поэтому для нашего примера мы создадим copyUser следующим образом:

func copyUser(u User) User {
   var lastName *string

   if u.LastName != nil {
      lastNameVal := *u.LastName
      lastName = &lastNameVal
   }

   var friends []User
   if u.Friends != nil {
      friends = make([]User, 0, len(u.Friends))
      for _, friend := range u.Friends {
         friends = append(friends, copyUser(friend))
      }
   }

   return User{
      Name:     u.Name,
      Id:       u.Id,
      LastName: lastName,
      Friends:  friends,
   }
}

где copyUser — рекурсивная функция, так как друзья также имеют тип пользователя.

Это решение будет наиболее производительным решением, а также будет правильно копировать все,

Проблемы с этим:

1: Много шаблонного кода (для каждого типа нам нужно будет создать функцию)

2: Разработчик может легко пропустить поля.

Поэтому я попытаюсь смягчить эти две ошибки и создать

Решение 3.5 (мое решение):

Я хочу сделать общий копир, чтобы не пропустить ни одного поля и использовать минимальный шаблонный код.

Итак, давайте разработаем нашу новую функцию.

func copyUser(u User) User{
 return User{}
}

Теперь воспользуемся Intellij для решения задачи №2:

Нажимаем ctrl+space для завершения кода.

И нажмите «заполнить все поля».

то мы получим это:

мы заметим, что для типов указателей мы получаем nil, а для типов без указателей мы получаем значение:

мы заполним другие значения следующим образом:

func copyUser(u User) User {
   return User{
      Name:     u.Name,
      LastName: nil,
      Id:       u.Id,
      Friends:  nil,
   }
}

Настало время дженериков, нам нужен способ копировать указатель или список в общем виде без записи для каждого типа, поэтому я создам эти универсальные функции: для получения дополнительной информации о дженериках см.:

https://medium.com/@aryehlevklein/golang-generics-101-and-creating-an-option-type-8a8cafdf407a

func copyPointer[T any](original *T, copier ...func(T) T) *T {
   if original == nil {
      return nil
   }

   var copyOfValue T
   if len(copier) > 0 {
      copyOfValue = copier[0](*original)
   } else {
      copyOfValue = *original
   }

   return &copyOfValue
}

эта функция скопирует значение в новый указатель.

Эту функцию можно использовать для любого типа указателя, кроме срезов и карт, что нам и нужно.

(функция копирования заключается в добавлении опции для более сложных базовых значений, которые также необходимо глубоко копировать, как мы скоро увидим)

То же самое проделаем для срезов:

func copySlice[T any](original []T, copier ...func(T) T) []T {
   if original == nil {
      return nil
   }

   var copyOfList = make([]T, len(original), len(original))

   for i := 0; i < len(original); i++ {
      if len(copier) > 0 {
         copyOfList[i] = copier[0](original[i])
      } else {
         copyOfList[i] = original[i]
      }

   }

   return copyOfList
}

и для карт:

func copyMap[K comparable, V any](original map[K]V, copier ...func(V) V) map[K]V {
   if original == nil {
      return nil
   }

   copyOfMap := make(map[K]V)

   for key, value := range original {
      if len(copier) > 0 {
         copyOfMap[key] = copier[0](value)
      } else {
         copyOfMap[key] = value
      }
   }

   return copyOfMap
}

Я не добавлял функцию копирования ключа, так как в большинстве случаев она не понадобится, но ее можно добавить.

Теперь мы можем переписать наш оригинальный copyUser:

func copyUser(u User) User {
 return User{
  Name:     u.Name,
  LastName: copyPointer(u.LastName),
  Id:       u.Id,
  Friends:  copySlice(u.Friends, copyUser),
 }
}

Мы значительно улучшили читаемость и сократили код!

Причина, по которой copySilce для друзей нуждается в функции копирования, заключается в том, что друзья — это часть пользователя, и пользователю также необходимо выполнять глубокое копирование.

Не убежден?

Создадим еще один тип:

type Transaction struct {
   AssociatedBanks []*Bank
   Name            *string

   Users map[string]User

   Dates []Date
}

type Bank struct {
   Id    *string
   money int
}

type Date struct {
   Days []*string
}

type User struct {
   Name     string
   LastName *string
   Id       string
   Friends  []User
}

А теперь давайте создадим нашу функцию копирования:

мы сначала используем автозаполнение intellij:

и получить эту функцию

func copyTransaction(old Transaction) Transaction {
   return Transaction{
      AssociatedBanks: nil,
      Name:            nil,
      Users:           nil,
      Dates:           nil,
      Me:              User{},
   }
}

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

func copyTransaction(old Transaction) Transaction {
   return Transaction{
      AssociatedBanks: copySlice(old.AssociatedBanks),
      Name:            copyPointer(old.Name),
      Users:           copyMap(old.Users),
      Dates:           copySlice(old.Dates),
      Me:              User{},
   }
}

Следующим шагом является создание функции deepCopy для всех подтипов, а также AssociatedBanks — это тип указателя в списке, поэтому его также необходимо глубоко скопировать.

поэтому давайте изменим его на:

func copyTransaction(old Transaction) Transaction {
   return Transaction{
      AssociatedBanks: copySlice(old.AssociatedBanks, func(b *Bank) *Bank {
         return copyPointer(b)
      }),
      Name:  copyPointer(old.Name),
      Users: copyMap(old.Users),
      Dates: copySlice(old.Dates),
      Me:    User{},
   }
}

подкопии с использованием той же техники:

func copyUser(u User) User {
 return User{
  Name:     u.Name,
  LastName: copyPointer(u.LastName),
  Id:       u.Id,
  Friends:  copySlice(u.Friends, copyUser),
 }
}

func copyDate(d Date) Date {
 return Date{
  Days: copySlice(d.Days, func(s *string) *string {
   return copyPointer(s)
  }),
 }
}

func copyBank(b Bank) Bank {
 return Bank{
  Id:    copyPointer(b.Id),
  money: b.money,
 }
}

Теперь переделываем функцию копирования транзакции:

func copyTransaction(old Transaction) Transaction {
   return Transaction{
      AssociatedBanks: copySlice(old.AssociatedBanks, func(b *Bank) *Bank {
         return copyPointer(b)
      }),
      Name:  copyPointer(old.Name),
      Users: copyMap(old.Users, copyUser),
      Dates: copySlice(old.Dates, copyDate),
      Me:    copyUser(old.Me),
   }
}

вот и все! около 20–30 строк кода, которые в основном заполняются автоматически и, на мой взгляд, очень понятны.

Я призываю вас попробовать сделать deepCopy for Transaction без использования этих функций и этого метода! :)

Весь код можно найти здесь:

https://github.com/aryehlev/deepCopy