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

Композиция структур Структуры в Go

Когда мы говорим о композиции в Go, речь идет о возможности включать одну структуру внутрь другой. Это похоже на конструктор из кубиков: у нас есть готовые детали (структуры), и мы собираем из них более сложные объекты.

Вместо наследования, как в объектно-ориентированных языках, Go использует встраивание структур (embedding). Это более простой и прозрачный механизм: одна структура может содержать другую в качестве поля. Причем если это поле указано без имени (только тип), его методы и поля становятся доступными напрямую у внешней структуры.

Пример

Допустим, у нас есть система для описания заказов в интернет-магазине. У каждого заказа есть данные о клиенте и адресе доставки.

Мы могли бы в Order скопировать все поля: имя, email, город, улицу и так далее. Но это приведет к дублированию: если те же самые данные нужны в другой сущности (например, для профиля пользователя), придется копировать все снова.

Вместо этого мы выделяем отдельные структуры и используем композицию:

type Customer struct {
    Name  string
    Email string
}

type Address struct {
    City   string
    Street string
}

type Order struct {
    ID       int
    Customer // встраиваем
    Address  // встраиваем
    Items    []string
    Status   string
}

Теперь Order автоматически получает доступ к полям Customer и Address:

func main() {
    order := Order{
        ID: 101,
        Customer: Customer{
            Name:  "Иван",
            Email: "ivan@example.com",
        },

        Address: Address{
            City:   "Москва",
            Street: "Ленина, 10",
        },

        Items:  []string{"Ноутбук", "Мышь"},
        Status: "new",
    }

    fmt.Println(order.Name)  // Иван
    fmt.Println(order.Email) // ivan@example.com
    fmt.Println(order.City)  // Москва
}

Мы можем обращаться к полям вложенных структур напрямую, будто они принадлежат Order. Это и есть "встраивание".

Зачем это нужно

  1. Избегаем дублирования кода. Если поля повторяются в нескольких местах, лучше вынести их в отдельную структуру и встроить.
  2. Логическая группировка. Поля собираются в отдельные сущности, которые отражают смысл задачи (например, Customer, Address).
  3. Расширяемость. Если завтра в адрес добавится индекс, нам не придется менять каждую структуру с адресом — достаточно изменить Address.
  4. Методы тоже наследуются. Если у Customer есть метод ContactInfo(), то он доступен и у Order.

Пример с методами

func (c Customer) ContactInfo() string {
    return fmt.Sprintf("%s <%s>", c.Name, c.Email)
}

func main() {
    order := Order{
        ID: 202,
        Customer: Customer{
            Name:  "Мария",
            Email: "maria@example.com",
        },
    }

    fmt.Println(order.ContactInfo()) // Мария <maria@example.com>
}

Метод ContactInfo определен у Customer, но мы можем вызвать его у Order напрямую, потому что Customer встроен.

Дополнительные примеры композиции структур

Логирование событий

В приложении часто нужно сохранять информацию о том, кто и когда сделал действие. Это удобно вынести в отдельную структуру:

type Audit struct {
    CreatedAt time.Time
    CreatedBy string
}

type User struct {
    ID   int
    Name string
    Audit
}

type Order struct {
    ID     int
    Status string
    Audit
}

Теперь и User, и Order автоматически имеют поля CreatedAt и CreatedBy.

func main() {
    user := User{
        ID:   1,
        Name: "Иван",
        Audit: Audit{
            CreatedAt: time.Now(),
            CreatedBy: "system",
        },
    }

    fmt.Println(user.CreatedAt) // время создания
    fmt.Println(user.CreatedBy) // system
}

Если бы мы не использовали композицию, то пришлось бы копировать поля CreatedAt и CreatedBy в каждую сущность — а таких сущностей в проекте может быть десятки.

Система прав доступа

Часто разные сущности в системе должны иметь одинаковую "обертку" с правами.

type Permissions struct {
    CanRead  bool
    CanWrite bool
    CanAdmin bool
}

type File struct {
    Name string
    Size int
    Permissions
}

type Project struct {
    Title string
    Permissions
}

Теперь и File, и Project содержат набор прав, и мы можем проверять их одинаково:

func main() {
    file := File{
        Name: "report.pdf",
        Permissions: Permissions{
            CanRead:  true,
            CanWrite: false,
        }}

    if file.CanRead {
        fmt.Println("Файл доступен для чтения")
    }
}

Здесь композиция позволяет легко распространять общую модель поведения (права доступа) на разные сущности.

Работа с геоданными

Представим систему для доставки. У каждой сущности может быть координата: склад, курьер, заказ.

type Location struct {
    Latitude  float64
    Longitude float64
}

type Warehouse struct {
    ID   int
    Name string
    Location
}

type Courier struct {
    ID   int
    Name string
    Location
}

type Delivery struct {
    ID int
    Courier
    Location
}

Теперь и склад, и курьер, и доставка имеют координаты, которые можно использовать в расчетах:

courier := Courier{
    Name: "Петр",
    Location: Location{
        Latitude:  55.75,
        Longitude: 37.61,
    }}
fmt.Println(courier.Latitude, courier.Longitude)
// 55.75 37.61

Если бы поля широты и долготы хранились отдельно в каждой структуре, обновлять или валидировать их было бы неудобно. А с общей структурой Location можно даже добавить метод DistanceTo и использовать его во всех местах.

Когда композицию использовать не стоит

Иногда разработчики "встраивают все подряд", и это вредит читаемости.

Дублирование без смысла

type Engine struct {
    Horsepower int
}

type Car struct {
    Brand  string
    Engine // встроили
    Wheels int
}

На первый взгляд нормально, но теперь у Car появилось поле Horsepower напрямую:

car := Car{
    Brand:  "BMW",
    Engine: Engine{Horsepower: 300},
    Wheels: 4}
fmt.Println(car.Horsepower) // выглядит как будто это поле машины

Проблема: читая код, можно подумать, что Horsepower — это характеристика Car, а не вложенного двигателя. Смысл потерялся.

Лучше явно оставить поле с именем:

type Car struct {
    Brand  string
    Engine Engine
    Wheels int
}

fmt.Println(car.Engine.Horsepower) // понятно, что это двигатель

Вложение ради «наследования»

Иногда новички пытаются использовать композицию как замену наследованию "животное → собака → пудель":

type Animal struct {
    Name string
}

type Dog struct {
    Animal
    Breed string
}

Теперь у Dog напрямую есть Name. Но если мы захотим сделать метод Speak() у Animal, а потом переопределить его у Dog, получится путаница с методами. Go специально не поддерживает наследование — композицию лучше применять для объединения свойств, а не для создания "иерархий классов".

Лучше так:

type Dog struct {
    Name  string
    Breed string
}

Просто у собаки есть Name, и это нормально. А если нужны разные сущности с именем, можно сделать интерфейс Named.

Конфликт имен

type Profile struct {
    Name string
}

type Company struct {
    Name string
}

type Employee struct {
    Profile
    Company
}

Теперь у Employee два Name, и при обращении employee.Name компилятор выдаст ошибку: «неоднозначность».

Лучше явно писать поля:

type Employee struct {
    Profile Profile
    Company Company
}

Теперь employee.Profile.Name и employee.Company.Name читаются без конфликтов.

Композиция — это один из ключевых приемов в Go. Она упрощает код, если использовать ее осознанно.

Хорошо применять, когда есть повторяющиеся поля, которые удобно вынести в отдельную структуру, или когда нужно логически сгруппировать данные. Это избавляет от дублирования и делает модель предметной области чище. — Плохо применять, если из-за встраивания код становится двусмысленным, теряется явная связь между сущностями или появляется конфликт имен. В таких случаях лучше использовать обычные поля.

Главное правило простое: композиция должна подчеркивать смысл и структуру задачи, а не запутывать ее.


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

Композиция помогает собирать крупные сущности из более мелких частей, а встраивание (embedding) даёт удобный прямой доступ к полям и методам вложенной структуры.

  1. Опишите структуру Publication с полями: Year int, Publisher string.
  2. Опишите структуру Book с полями: название, имя автора и встроенной структурой Publication.
  3. Создайте одну книгу и выведите: название, автора и издателя.
Показать пример ответа
package main

import "fmt"

type Publication struct {
    Year      int
    Publisher string
}

type Book struct {
    Title       string
    Author      string
    Publication // встраивание
}

func main() {
    b := Book{
        Title:  "Алгоритмы",
        Author: "Петров",
        Publication: Publication{
            Year:      2023,
            Publisher: "TechBooks",
        },
    }

    fmt.Printf("%s — %s (%s, %d)\n", b.Title, b.Author, b.Publisher, b.Year)
}

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

  1. Effective Go — Embedding
  2. Go Spec — Struct types (embedded fields)
  3. Go Spec — Method sets

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

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

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

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

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

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

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

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