Go. Структуры данных
Вложенные структуры и встраивание (composition)
Представьте, что вы создаёте систему для управления сотрудниками компании. У каждого сотрудника есть имя, адрес, контактная информация, должность. Вы можете создать одну большую структуру со всеми полями, но это быстро станет громоздким.
Что если несколько типов сотрудников используют одинаковые данные? Что если вам нужно переиспользовать логику работы с адресами в других частях приложения?
В Go есть два способа комбинирования структур: вложенность и встраивание. Вложенность — это когда одна структура содержит другую как обычное поле. Встраивание (embedding) — это особая возможность Go, которая позволяет «наследовать» поля и методы другой структуры, делая код более компактным и выразительным.
В этой лекции мы разберём оба подхода, поймём разницу между ними и научимся выбирать правильный инструмент для каждой задачи.
Содержание
Вложенные структуры
Базовое определение
Вложенная структура — это когда одна структура содержит другую структуру как именованное поле. Это самый прямолинейный способ композиции:
type Address struct {
    Street  string
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address Address  // ← Вложенная структура
}
В этом примере структура Person содержит поле Address типа Address. Это обычное поле, как Name или Age, просто его тип — это другая структура.
Доступ к полям вложенных структур
Для доступа к полям вложенной структуры используется цепочка через точку:
func main() {
    p := Person{
        Name: "Alice",
        Age:  30,
        Address: Address{
            Street:  "123 Main St",
            City:    "New York",
            Country: "USA",
        },
    }
    
    fmt.Println(p.Name)              // ← Alice
    fmt.Println(p.Address.City)      // ← New York
    fmt.Println(p.Address.Country)   // ← USA
}
Чтобы получить город, мы пишем p.Address.City — сначала обращаемся к полю Address, потом к его полю City. Это явная иерархия, которая чётко показывает структуру данных.
Инициализация вложенных структур
Вложенные структуры можно инициализировать несколькими способами:
// Способ 1: инициализация всех полей сразу
p1 := Person{
    Name: "Bob",
    Age:  25,
    Address: Address{
        Street:  "456 Oak Ave",
        City:    "Boston",
        Country: "USA",
    },
}

// Способ 2: пошаговая инициализация
p2 := Person{
    Name: "Charlie",
    Age:  35,
}
p2.Address.Street = "789 Pine Rd"
p2.Address.City = "Seattle"
p2.Address.Country = "USA"

// Способ 3: создание адреса отдельно
addr := Address{
    Street:  "321 Elm St",
    City:    "Chicago",
    Country: "USA",
}
p3 := Person{
    Name:    "Diana",
    Age:     28,
    Address: addr,
}
Все три способа валидны, выбор зависит от контекста и удобства.
Встраивание структур
Что такое embedding?
Встраивание (embedding) — это когда структура содержит другую структуру как безымянное поле. В этом случае поля и методы встроенной структуры становятся доступны напрямую:
type Address struct {
    Street  string
    City    string
    Country string
}

type Person struct {
    Address  // ← Встроенная структура (без имени поля)
    Name    string
    Age     int
}
Обратите внимание: мы написали просто Address, без имени поля. Это встраивание. Теперь поля структуры Address становятся доступны напрямую на уровне Person.
Встраивание полей
При встраивании поля встроенной структуры становятся доступны так, как будто они являются полями внешней структуры:
func main() {
    p := Person{
        Name: "Alice",
        Age:  30,
    }
    
    // Можно обращаться к полям Address напрямую
    p.Street = "123 Main St"
    p.City = "New York"
    p.Country = "USA"
    
    fmt.Println(p.City)  // ← New York
    
    // Но также можно обращаться через имя типа
    fmt.Println(p.Address.City)  // ← New York
}
Поля Street, City, Country технически принадлежат Address, но Go позволяет обращаться к ним напрямую через Person. При этом полный путь p.Address.City также работает. В Go это называется field promotion.
Важно понимать, что встраивание — это не наследование в классическом понимании. Структура Person не «является» Address, она «содержит» Address. Это композиция, а не наследование.
Встраивание методов
Встраивание делает доступными напрямую не только поля, но и методы встроенной структуры:
type Address struct {
    Street  string
    City    string
    Country string
}

func (a Address) FullAddress() string {
    return fmt.Sprintf("%s, %s, %s", a.Street, a.City, a.Country)
}

type Person struct {
    Address
    Name string
    Age  int
}

func main() {
    p := Person{
        Name: "Alice",
        Age:  30,
        Address: Address{
            Street:  "123 Main St",
            City:    "New York",
            Country: "USA",
        },
    }
    
    // Метод Address доступен напрямую через Person
    fmt.Println(p.FullAddress())  // ← 123 Main St, New York, USA
}
Метод FullAddress определён для типа Address, но благодаря встраиванию мы можем вызвать его на объекте типа Person. Это мощный механизм для переиспользования функциональности.
Встраивание vs вложенные структуры
Когда использовать встраивание, а когда — вложенные структуры? Вот основные критерии:
Используйте встраивание, когда:
Внешняя структура логически расширяет функциональность встроенной. Например, Manager расширяет Employee дополнительными полями и методами:
type Employee struct {
    ID   int
    Name string
}

func (e Employee) GetInfo() string {
    return fmt.Sprintf("Employee #%d: %s", e.ID, e.Name)
}

type Manager struct {
    Employee  // ← Manager "является" Employee с дополнительными свойствами
    Team []string
}

func (m Manager) GetTeamSize() int {
    return len(m.Team)
}
Используйте вложенные структуры, когда:
Структуры логически независимы, и одна просто «имеет» другую как часть данных. Например, Order содержит Customer, но заказ — это не расширенный покупатель:
type Customer struct {
    ID    int
    Name  string
    Email string
}

type Order struct {
    ID       int
    Customer Customer  // ← Order "имеет" Customer
    Items    []string
    Total    float64
}
В этом случае было бы странно писать order.Name вместо order.Customer.Name. Явное использование вложенной структуры делает код более понятным.
Множественное встраивание
Go позволяет встраивать несколько структур одновременно:
type Printable struct{}

func (p Printable) Print() {
    fmt.Println("Printing...")
}

type Saveable struct{}

func (s Saveable) Save() {
    fmt.Println("Saving...")
}

type Document struct {
    Printable  // ← Первое встраивание
    Saveable   // ← Второе встраивание
    Title   string
    Content string
}

func main() {
    doc := Document{Title: "Report", Content: "..."}
    doc.Print()  // ← Метод от Printable
    doc.Save()   // ← Метод от Saveable
}
Это позволяет комбинировать функциональность из разных источников. Document получает возможности и печати, и сохранения.
Конфликты имён при встраивании
Что происходит, если несколько встроенных структур имеют поля или методы с одинаковыми именами? Go разрешает конфликты по простому правилу: если конфликт не разрешён явно, доступ к этому полю или методу вызовет ошибку компиляции.
type A struct {
    Value int
}

func (a A) Display() {
    fmt.Println("Display from A, Value:", a.Value)
}

type B struct {
    Value int
}

func (b B) Display() {
    fmt.Println("Display from B, Value:", b.Value)
}

type C struct {
    A
    B
}

func main() {
    c := C{}
    
    // Конфликт полей
    // c.Value = 10  // ← Ошибка компиляции: ambiguous selector c.Value
    c.A.Value = 10   // ← Нужно явно указать, какой Value
    c.B.Value = 20
    
    // Конфликт методов
    // c.Display()      // ← Ошибка компиляции: ambiguous selector c.Display
    c.A.Display()       // ← Display from A, Value: 10
    c.B.Display()       // ← Display from B, Value: 20
}
Компилятор не может выбрать между A.Value и B.Value, равно как и между методами A.Display() и B.Display(). Единственный способ разрешить конфликт — явно указать путь к нужному полю или методу через имя типа.
Затенение полей и методов
Если внешняя структура определяет поле или метод с тем же именем, оно «затеняет» поле/метод встроенной структуры:
type Celsius float64
type Fahrenheit float64

func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
    return Celsius((f - 32) * 5 / 9)
}

func main() {
    temp := Celsius(20.0)
    fmt.Printf("%.1f°C = %.1f°F\n", temp, temp.ToFahrenheit())  // ← 20.0°C = 68.0°F
}
В этом примере d.Value обращается к полю Derived, а d.Base.Value — к полю Base. Метод Print работает с Base.Value, потому что он определён для типа Base.
Заключение
Композиция через вложенные структуры и встраивание — это основной способ организации кода в Go. В отличие от классического наследования, композиция более гибкая и явная.
Ключевые моменты:
  • Вложенные структуры — это обычные именованные поля с типом-структурой
  • Встраивание (embedding) — это безымянные поля, которые делают свои поля и методы доступными напрямую на уровне внешней структуры
  • Используйте встраивание для расширения функциональности, вложенные структуры — для отношений «имеет»
  • Множественное встраивание позволяет комбинировать функциональность из разных источников
  • Конфликты имён разрешаются через явное указание пути к полю
Что дальше?
Теперь вы готовы изучать следующие темы:
  • Интерфейсы
    Объявление и реализация, полиморфизм
  • Интерфейсы
    Внутренее устройство
Поздравляю, вы только что освоили композицию структур в Go!
Это знание поможет вам строить гибкую архитектуру приложений, переиспользуя код через вложенные структуры и встраивание.