Структуры в Go
Теория: Сравнение структур и копирование
Когда мы начинаем писать код на Go, структуры кажутся простыми: собрали в кучу поля — и можно хранить данные о пользователе, заказе, продукте. Но как только дело доходит до сравнения и копирования этих структур, легко запутаться. Почему сравнение иногда работает, а иногда ломается? Почему после копирования данные меняются сразу в двух местах? Давайте разбираться, начиная с самых простых примеров, и постепенно дойдем до реальной практики.
Сравнение структур с примитивами
Представим интернет-магазин. У нас есть товар с идентификатором и ценой:
Создадим два одинаковых товара:
Все работает прозрачно: Go сравнил поля по значениям. ID и Price совпали — значит структуры равны.
Важно: это сравнение работает, потому что все поля структуры — простые типы: числа, строки, bool.
Где все ломается: срезы и карты
Теперь представь, что к товару мы добавили список тегов. Это срез ([]string):
Создадим два товара с одинаковыми тегами:
И тут программа даже не запускается. Ошибка: «struct containing []string cannot be compared».
Почему так? Потому что срез — это не сами данные, а «бумажка с адресом массива». Два разных среза могут указывать на один и тот же массив, а могут на разные. Компилятор не хочет гадать, что считать равенством. Поэтому Go запрещает такое сравнение.
Как правильно сравнивать сложные структуры
В реальности нам часто приходится писать свой метод Equal. Так мы сами задаем правила равенства:
Теперь проверим равенство так:
👉 В тестах часто используют reflect.DeepEqual, потому что это быстрее в написании:
Но DeepEqual имеет особенности: nil и пустой срез он считает разными. Поэтому в бизнес-коде лучше писать Equal, а в тестах можно использовать DeepEqual ради скорости.
Копирование: простые структуры
Теперь перейдем к копированию. Начнем снова с простого.
Здесь a и b независимы. Все работает как ожидаешь: Go взял и скопировал поля по значениям.
Подвох: структуры со ссылочными полями
Теперь добавим список товаров:
Скопируем заказ:
Вот тут подвох. Вроде бы сделали копию, но a и b смотрят на один и тот же массив товаров.
Почему? Потому что у структуры скопировалась только «бумажка с адресом массива». Оба заказа теперь указывают на один и тот же список товаров.
Глубокое копирование
Если нам нужна независимая копия — придется руками копировать данные.
Теперь массивы разные, и изменения не пересекаются.
Глубокая копия нужна не только для срезов, но и для map и указателей.
Пример:
Передача структур в функции
В Go структуры передаются в функции по значению — то есть копируются.
Оригинал не изменился.
Если нужно менять данные в оригинале — передаем указатель:
Реальные сценарии из работы
Тестирование API
Мы написали функцию, которая возвращает User. В тесте хотим проверить, что результат правильный. Если User содержит только примитивы — сравниваем напрямую. Если там есть срезы или карты — пишем метод Equal.
Конфиги
В сервисах часто есть глобальный Config. Иногда нужно сделать его копию, поменять пару значений и проверить что-то. Если забыть про глубокое копирование, изменения утекут в глобальный конфиг. Один модуль поменяет параметр «для себя», а другой внезапно начнет работать по-новому. Это типичный источник багов.
Воркеры и горутины
У нас есть заказ с товарами. Мы запускаем несколько горутин и копируем заказ в каждую, думая, что они независимы. Но если в заказе есть срез, все горутины начинают работать с одним и тем же массивом. Итог — гонки данных и хаос. Решение — делать глубокие копии.
Практические паттерны: Equal и Clone
Чтобы не каждый раз думать «а что там скопируется, а что нет» или «как корректно сравнить два объекта», в больших проектах у структур часто делают два обязательных метода:
Equal()— определяет, равны ли два объекта.Clone()— создает честную копию объекта.
Пример: структура пользователя
Допустим, у нас есть пользователь с тегами и атрибутами:
Метод Equal
Теперь можно спокойно сравнивать:
Здесь мы сами задали правила сравнения. Нет неожиданностей вроде «порядок ключей в мапе разный».
Метод Clone
Теперь мы получаем реально независимый объект:
👉 Теперь u1 и u2 никак не зависят друг от друга.
Зачем так делать?
- В тестах
Equalдает точное определение равенства. - В бизнес-логике
Cloneпозволяет безопасно работать с копиями, не ломая глобальное состояние. - Команда не тратит время на догадки — все знают, что у каждой важной структуры есть свои правила «равенства» и «копирования».
Если структура простая — можно обойтись без этих методов. Но как только в ней появляются срезы или мапы, сразу добавляй Equal и Clone. Это избавит от десятков мелких и больших багов в будущем.
Готовые функции для сравнения и копирования
В стандартной библиотеке Go есть удобные пакеты slices и maps, которые решают многие из описанных выше проблем.
Сравнение срезов:
Эта функция корректно сравнивает содержимое срезов. Больше не нужно писать цикл вручную.
Клонирование среза:
Функция slices.Clone создает новый массив внутри, поэтому исходный и копия не влияют друг на друга.
Сравнение карт:
Порядок ключей в map не имеет значения — maps.Equal сравнивает именно пары ключ-значение.
Клонирование карты:
Функция maps.Clone создает новую карту и копирует в нее все пары. Теперь можно работать с копией, не боясь сломать оригинал.
По сути, это «готовые» версии тех же приемов, что мы писали вручную. Они делают код чище и безопаснее.


