Зарегистрируйтесь, чтобы продолжить обучение

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

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


Самостоятельная работа

Закрепим разницу между сравнимыми и несравнимыми структурами и потренируемся делать «глубокие» копии ссылочных полей.

Задачи:

  1. Равенство простых структур.

    • Опишите структуру Book с полями Title string, Author string.
    • Создайте две одинаковые книги и сравните оператором == — выведите результат.
  2. Несравнимость при срезах.

    • Добавьте в Book поле Tags []string и попробуйте снова сравнить две книги ==. Что произойдет и почему?
    • Реализуйте метод Equal(other Book) bool, который сравнивает все поля, включая элементы среза Tags.
  3. Глубокая копия среза

    • Создайте 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]
}

Дополнительные материалы

  1. Go Spec — Comparison operators
  2. reflect.DeepEqual — docs
  3. Go Blog — Slices: usage and internals (копирование и ссылки)
  4. Go Spec — Appending and copying slices

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff