- Сравнение структур с примитивами
- Где все ломается: срезы и карты
- Как правильно сравнивать сложные структуры
- Копирование: простые структуры
- Подвох: структуры со ссылочными полями
- Глубокое копирование
- Передача структур в функции
- Реальные сценарии из работы
- Практические паттерны: Equal и Clone
Когда мы начинаем писать код на Go, структуры кажутся простыми: собрали в кучу поля — и можно хранить данные о пользователе, заказе, продукте. Но как только дело доходит до сравнения и копирования этих структур, легко запутаться. Почему сравнение иногда работает, а иногда ломается? Почему после копирования данные меняются сразу в двух местах? Давайте разбираться, начиная с самых простых примеров, и постепенно дойдем до реальной практики.
Сравнение структур с примитивами
Представим интернет-магазин. У нас есть товар с идентификатором и ценой:
type Product struct {
ID int
Price int
}
Создадим два одинаковых товара:
p1 := Product{ID: 1, Price: 100}
p2 := Product{ID: 1, Price: 100}
p3 := Product{ID: 2, Price: 200}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false
Все работает прозрачно: Go сравнил поля по значениям. ID
и Price
совпали — значит структуры равны.
Важно: это сравнение работает, потому что все поля структуры — простые типы: числа, строки, bool.
Где все ломается: срезы и карты
Теперь представь, что к товару мы добавили список тегов. Это срез ([]string
):
type Product struct {
ID int
Price int
Tags []string
}
Создадим два товара с одинаковыми тегами:
p1 := Product{ID: 1, Price: 100, Tags: []string{"sale", "popular"}}
p2 := Product{ID: 1, Price: 100, Tags: []string{"sale", "popular"}}
fmt.Println(p1 == p2) // ошибка компиляции
И тут программа даже не запускается. Ошибка: «struct containing []string cannot be compared».
Почему так? Потому что срез — это не сами данные, а «бумажка с адресом массива». Два разных среза могут указывать на один и тот же массив, а могут на разные. Компилятор не хочет гадать, что считать равенством. Поэтому Go запрещает такое сравнение.
Как правильно сравнивать сложные структуры
В реальности нам часто приходится писать свой метод Equal
. Так мы сами задаем правила равенства:
type Product struct {
ID int
Price int
Tags []string
}
// Equal проверяет равенство товаров
func (p Product) Equal(other Product) bool {
if p.ID != other.ID || p.Price != other.Price {
return false
}
if len(p.Tags) != len(other.Tags) {
return false
}
for i := range p.Tags {
if p.Tags[i] != other.Tags[i] {
return false
}
}
return true
}
Теперь проверим равенство так:
p1 := Product{ID: 1, Price: 100, Tags: []string{"sale", "popular"}}
p2 := Product{ID: 1, Price: 100, Tags: []string{"sale", "popular"}}
fmt.Println(p1.Equal(p2)) // true
👉 В тестах часто используют reflect.DeepEqual
, потому что это быстрее в написании:
fmt.Println(reflect.DeepEqual(p1, p2)) // true
Но DeepEqual
имеет особенности: nil
и пустой срез он считает разными. Поэтому в бизнес-коде лучше писать Equal
, а в тестах можно использовать DeepEqual
ради скорости.
Копирование: простые структуры
Теперь перейдем к копированию. Начнем снова с простого.
type Order struct {
ID int
Status string
}
a := Order{ID: 1, Status: "new"}
b := a // копия
b.Status = "paid"
fmt.Println("a:", a.Status) // new
fmt.Println("b:", b.Status) // paid
Здесь a
и b
независимы. Все работает как ожидаешь: Go взял и скопировал поля по значениям.
Подвох: структуры со ссылочными полями
Теперь добавим список товаров:
type Order struct {
ID int
Items []string
}
Скопируем заказ:
a := Order{ID: 1, Items: []string{"Телефон", "Мышь"}}
b := a // копия
b.Items[0] = "Ноутбук"
fmt.Println("a:", a.Items) // [Ноутбук Мышь]
fmt.Println("b:", b.Items) // [Ноутбук Мышь]
Вот тут подвох. Вроде бы сделали копию, но a
и b
смотрят на один и тот же массив товаров.
Почему? Потому что у структуры скопировалась только «бумажка с адресом массива». Оба заказа теперь указывают на один и тот же список товаров.
Глубокое копирование
Если нам нужна независимая копия — придется руками копировать данные.
a := Order{ID: 1, Items: []string{"Телефон", "Мышь"}}
// создаем новый срез
copiedItems := make([]string, len(a.Items))
copy(copiedItems, a.Items)
b := Order{ID: a.ID, Items: copiedItems}
b.Items[0] = "Ноутбук"
fmt.Println("a:", a.Items) // [Телефон Мышь]
fmt.Println("b:", b.Items) // [Ноутбук Мышь]
Теперь массивы разные, и изменения не пересекаются.
Глубокая копия нужна не только для срезов, но и для map и указателей.
Пример:
type Profile struct {
Name string
Tags []string
Meta map[string]string
Addr *Address
}
type Address struct {
City string
Zip string
}
func (p Profile) Clone() Profile {
tags := make([]string, len(p.Tags))
copy(tags, p.Tags)
meta := make(map[string]string, len(p.Meta))
for k, v := range p.Meta {
meta[k] = v
}
var addr *Address
if p.Addr != nil {
a := *p.Addr
addr = &a
}
return Profile{
Name: p.Name,
Tags: tags,
Meta: meta,
Addr: addr,
}
}
Передача структур в функции
В Go структуры передаются в функции по значению — то есть копируются.
type User struct {
Name string
Age int
}
func update(u User) {
u.Age = 99
}
user := User{Name: "Иван", Age: 30}
update(user)
fmt.Println(user.Age) // 30
Оригинал не изменился.
Если нужно менять данные в оригинале — передаем указатель:
func updatePtr(u *User) {
u.Age = 99
}
updatePtr(&user)
fmt.Println(user.Age) // 99
Реальные сценарии из работы
Тестирование API
Мы написали функцию, которая возвращает User
. В тесте хотим проверить, что результат правильный. Если User
содержит только примитивы — сравниваем напрямую. Если там есть срезы или карты — пишем метод Equal
.
Конфиги
В сервисах часто есть глобальный Config
. Иногда нужно сделать его копию, поменять пару значений и проверить что-то. Если забыть про глубокое копирование, изменения утекут в глобальный конфиг. Один модуль поменяет параметр «для себя», а другой внезапно начнет работать по-новому. Это типичный источник багов.
Воркеры и горутины
У нас есть заказ с товарами. Мы запускаем несколько горутин и копируем заказ в каждую, думая, что они независимы. Но если в заказе есть срез, все горутины начинают работать с одним и тем же массивом. Итог — гонки данных и хаос. Решение — делать глубокие копии.
Практические паттерны: Equal и Clone
Чтобы не каждый раз думать «а что там скопируется, а что нет» или «как корректно сравнить два объекта», в больших проектах у структур часто делают два обязательных метода:
Equal()
— определяет, равны ли два объекта.Clone()
— создает честную копию объекта.
Пример: структура пользователя
Допустим, у нас есть пользователь с тегами и атрибутами:
type User struct {
ID int
Name string
Tags []string
Props map[string]string
}
Метод Equal
func (u User) Equal(other User) bool {
// сравниваем простые поля
if u.ID != other.ID || u.Name != other.Name {
return false
}
// сравниваем срез
if len(u.Tags) != len(other.Tags) {
return false
}
for i := range u.Tags {
if u.Tags[i] != other.Tags[i] {
return false
}
}
// сравниваем карту
if len(u.Props) != len(other.Props) {
return false
}
for k, v := range u.Props {
if other.Props[k] != v {
return false
}
}
return true
}
Теперь можно спокойно сравнивать:
u1 := User{ID: 1, Name: "Иван", Tags: []string{"go"}, Props: map[string]string{"lang": "ru"}}
u2 := User{ID: 1, Name: "Иван", Tags: []string{"go"}, Props: map[string]string{"lang": "ru"}}
fmt.Println(u1.Equal(u2)) // true
Здесь мы сами задали правила сравнения. Нет неожиданностей вроде «порядок ключей в мапе разный».
Метод Clone
func (u User) Clone() User {
// копируем срез
newTags := make([]string, len(u.Tags))
copy(newTags, u.Tags)
// копируем мапу
newProps := make(map[string]string, len(u.Props))
for k, v := range u.Props {
newProps[k] = v
}
// собираем копию
return User{
ID: u.ID,
Name: u.Name,
Tags: newTags,
Props: newProps,
}
}
Теперь мы получаем реально независимый объект:
u1 := User{
ID: 1, Name: "Иван",
Tags: []string{"go"},
Props: map[string]string{"lang": "ru"}}
u2 := u1.Clone()
u2.Tags[0] = "python"
u2.Props["lang"] = "en"
fmt.Println(u1.Tags) // [go]
fmt.Println(u2.Tags) // [python]
fmt.Println(u1.Props) // map[lang:ru]
fmt.Println(u2.Props) // map[lang:en]
👉 Теперь u1
и u2
никак не зависят друг от друга.
Зачем так делать?
- В тестах
Equal
дает точное определение равенства. - В бизнес-логике
Clone
позволяет безопасно работать с копиями, не ломая глобальное состояние. - Команда не тратит время на догадки — все знают, что у каждой важной структуры есть свои правила «равенства» и «копирования».
Если структура простая — можно обойтись без этих методов. Но как только в ней появляются срезы или мапы, сразу добавляй Equal и Clone. Это избавит от десятков мелких и больших багов в будущем.
Готовые функции для сравнения и копирования
В стандартной библиотеке Go есть удобные пакеты slices и maps, которые решают многие из описанных выше проблем.
Сравнение срезов:
import "slices"
func main() {
a := []int{1, 2, 3}
b := []int{1, 2, 3}
fmt.Println(slices.Equal(a, b)) // true
}
Эта функция корректно сравнивает содержимое срезов. Больше не нужно писать цикл вручную.
Клонирование среза:
clone := slices.Clone(a)
clone[0] = 99
fmt.Println(a) // [1 2 3]
fmt.Println(clone) // [99 2 3]
Функция slices.Clone
создает новый массив внутри, поэтому исходный и копия не влияют друг на друга.
Сравнение карт:
import "maps"
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 2, "a": 1}
fmt.Println(maps.Equal(m1, m2)) // true
}
Порядок ключей в map не имеет значения — maps.Equal
сравнивает именно пары ключ-значение.
Клонирование карты:
copyMap := maps.Clone(m1)
copyMap["a"] = 99
fmt.Println(m1) // map[a:1 b:2]
fmt.Println(copyMap) // map[a:99 b:2]
Функция maps.Clone
создает новую карту и копирует в нее все пары. Теперь можно работать с копией, не боясь сломать оригинал.
По сути, это «готовые» версии тех же приемов, что мы писали вручную. Они делают код чище и безопаснее.
Самостоятельная работа
Закрепим разницу между сравнимыми и несравнимыми структурами и потренируемся делать «глубокие» копии ссылочных полей.
Задачи:
Равенство простых структур.
- Опишите структуру
Book
с полямиTitle string
,Author string
. - Создайте две одинаковые книги и сравните оператором
==
— выведите результат.
- Опишите структуру
Несравнимость при срезах.
- Добавьте в
Book
полеTags []string
и попробуйте снова сравнить две книги==
. Что произойдет и почему? - Реализуйте метод
Equal(other Book) bool
, который сравнивает все поля, включая элементы срезаTags
.
- Добавьте в
Глубокая копия среза
- Создайте
b1 := Book{Title: "Go", Author: "Alan", Tags: []string{"lang","guide"}}
. - Создайте независимую копию
b2
, гдеTags
— новый срез с теми же значениями (используйтеmake
иcopy
). - Измените
b2.Tags[0]
и убедитесь, чтоb1.Tags
не изменился.
- Создайте
Показать пример ответа
package main
import "fmt"
type Book struct {
Title string
Author string
Tags []string
}
// Equal сравнивает книги по всем полям, включая содержимое среза.
func (b Book) Equal(o Book) bool {
if b.Title != o.Title || b.Author != o.Author {
return false
}
if len(b.Tags) != len(o.Tags) {
return false
}
for i := range b.Tags {
if b.Tags[i] != o.Tags[i] {
return false
}
}
return true
}
func main() {
// 1) Примитивные поля — сравнимо
bA := Book{Title: "Go", Author: "Alan"}
bB := Book{Title: "Go", Author: "Alan"}
fmt.Println(bA == bB) // true (если временно убрать Tags из типа)
// 2) Срез делает структуру несравнимой оператором == (ошибка компиляции)
// fmt.Println(bA == bB)
// 3) Глубокая копия среза
b1 := Book{Title: "Go", Author: "Alan", Tags: []string{"lang", "guide"}}
// создаём новый срез и копируем элементы
tags := make([]string, len(b1.Tags))
copy(tags, b1.Tags)
b2 := Book{Title: b1.Title, Author: b1.Author, Tags: tags}
b2.Tags[0] = "doc"
fmt.Println(b1.Tags) // [lang guide]
fmt.Println(b2.Tags) // [doc guide]
}
Дополнительные материалы
- Go Spec — Comparison operators
- reflect.DeepEqual — docs
- Go Blog — Slices: usage and internals (копирование и ссылки)
- Go Spec — Appending and copying slices
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.