Структуры в 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. Она упрощает код, если использовать ее осознанно.

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

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

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

+7 800 100 22 47

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

+7 495 085 21 62

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

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