- Пример
- Зачем это нужно
- Пример с методами
- Дополнительные примеры композиции структур
- Когда композицию использовать не стоит
Когда мы говорим о композиции в 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
. Это и есть "встраивание".
Зачем это нужно
- Избегаем дублирования кода. Если поля повторяются в нескольких местах, лучше вынести их в отдельную структуру и встроить.
- Логическая группировка. Поля собираются в отдельные сущности, которые отражают смысл задачи (например,
Customer
,Address
). - Расширяемость. Если завтра в адрес добавится индекс, нам не придется менять каждую структуру с адресом — достаточно изменить
Address
. - Методы тоже наследуются. Если у 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) даёт удобный прямой доступ к полям и методам вложенной структуры.
- Опишите структуру
Publication
с полями:Year int
,Publisher string
. - Опишите структуру
Book
с полями: название, имя автора и встроенной структуройPublication
. - Создайте одну книгу и выведите: название, автора и издателя.
Показать пример ответа
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)
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.