Структуры в Go

Теория: Сравнение структур и копирование

Когда мы начинаем писать код на 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

Чтобы не каждый раз думать «а что там скопируется, а что нет» или «как корректно сравнить два объекта», в больших проектах у структур часто делают два обязательных метода:

  1. Equal() — определяет, равны ли два объекта.
  2. 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 создает новую карту и копирует в нее все пары. Теперь можно работать с копией, не боясь сломать оригинал.

По сути, это «готовые» версии тех же приемов, что мы писали вручную. Они делают код чище и безопаснее.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845