Что такое глубокое копирование?
Глубокая копия — это когда мы копируем объект, а новый и старый объекты не используют общую память.
что это значит?
давайте начнем с примера, чтобы увидеть, каков вариант использования.
В этом примере мы будем иметь дело с серверной частью социальных сетей.
Структура, с которой мы будем работать, — это 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 ©OfValue }
эта функция скопирует значение в новый указатель.
Эту функцию можно использовать для любого типа указателя, кроме срезов и карт, что нам и нужно.
(функция копирования заключается в добавлении опции для более сложных базовых значений, которые также необходимо глубоко копировать, как мы скоро увидим)
То же самое проделаем для срезов:
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 без использования этих функций и этого метода! :)
Весь код можно найти здесь: