Встраивание структур (composition over inheritance)
На собеседовании спросят: «В Go нет наследования — как тогда переиспользовать код между структурами?»
Встраивание (embedding) — включение одной структуры в другую без имени поля. Внешняя структура «наследует» доступ к полям и методам внутренней, но это композиция, а не классическое наследование.
Что выведет этот код?
Посмотрим на пример с двигателем и машиной:
type Engine struct {
Power int
}
func (e Engine) Start() string {
return fmt.Sprintf("Двигатель на %d л.с. заведён", e.Power)
}
type Car struct {
Engine // встраивание (embedding), без имени поля
Model string
}
func main() {
c := Car{
Engine: Engine{Power: 150},
Model: "Волга",
}
fmt.Println(c.Model, "-", c.Start())
fmt.Println("Мощность напрямую:", c.Power)
}Вывод:
Волга - Двигатель на 150 л.с. заведён
Мощность напрямую: 150Обратите внимание на две детали. Во-первых, поле Engine в структуре Car объявлено без имени — только тип. Это и есть встраивание. Во-вторых, мы вызываем c.Start() и читаем c.Power напрямую у Car, хотя метод Start и поле Power на самом деле принадлежат Engine.
Как это работает под капотом
Когда вы встраиваете структуру без явного имени поля, Go автоматически даёт полю имя, совпадающее с именем типа — то есть Car получает поле Engine (можно писать c.Engine.Power явно). Но дальше срабатывает механизм, который называется продвижение (promotion): все методы и поля встроенного типа становятся доступны напрямую у внешней структуры, как будто они её собственные.
Это не наследование в смысле Java или C++. Внутри Car буквально лежит копия (или указатель, если встраивать *Engine) структуры Engine как обычное поле — просто без явного имени. Компилятор при обращении c.Start() сам «доходит» до вложенного поля и вызывает метод на нём. Это композиция (структура состоит из другой структуры) с синтаксическим сахаром сверху, который делает обращение таким же удобным, как при наследовании. Отсюда девиз Go-сообщества: composition over inheritance — «композиция вместо наследования».
У такого подхода есть практическое преимущество: можно встроить сразу несколько типов (в отличие от одиночного наследования во многих языках), и структура получит методы от всех сразу — без диких проблем ромбовидного наследования, потому что здесь просто нет иерархии классов как таковой.
Ловушка: что будет, если имена методов совпадут?
А что если у внешней структуры определить метод с таким же именем, как у встроенной? Чему отдаётся приоритет?
type Base struct{}
func (b Base) Greet() string {
return "Привет из Base"
}
type Derived struct {
Base
}
func (d Derived) Greet() string {
return "Привет из Derived"
}
func main() {
d := Derived{}
fmt.Println(d.Greet()) // какой метод вызовется?
fmt.Println(d.Base.Greet()) // а если явно уточнить путь?
}Вывод:
Привет из Derived
Привет из BaseМетод, объявленный непосредственно у Derived, «затеняет» (shadowing) одноимённый продвинутый метод из Base. Правило простое: чем ближе метод объявлен к внешней структуре, тем выше его приоритет. Если нужно достучаться именно до версии встроенного типа — указывайте путь явно, через имя встроенного поля: d.Base.Greet().
Это принципиально отличается от переопределения (override) в классическом ООП. В Java или C++ вызов метода базового класса из объекта производного класса через виртуальный вызов приведёт к вызову переопределённой версии — динамический полиморфизм. В Go никакого полиморфизма здесь нет: Base.Greet() ничего не знает о существовании Derived и никогда не вызовет её версию метода, даже если вызывать Greet изнутри метода самого Base. Затенение работает только «снаружи», на уровне того, к какому методу обращается вызывающий код.
Встраивание и интерфейсы
Есть ещё один важный эффект: если встроенный тип реализует какой-то интерфейс, то и внешняя структура автоматически начинает его реализовывать — благодаря продвижению методов. Например, если Engine реализует интерфейс с методом Start() string, то Car тоже будет удовлетворять этому интерфейсу, даже не объявляя собственного метода Start. Этим приёмом часто пользуются, чтобы «дособрать» нужный интерфейс из готовых кусочков.
Частые ошибки на собеседовании
- Называют embedding наследованием буквально. Это композиция с продвижением методов, а не наследование — нет ни виртуальных вызовов, ни переопределения в классическом смысле.
- Не могут объяснить, что произойдёт при совпадении имён методов. Побеждает метод, объявленный ближе к внешнему типу — работает как затенение, а не слияние.
- Путают встраивание структуры (embedding) с обычным именованным полем.
type Car struct { Engine Engine }— это обычное поле, методыEngineне продвигаются, нужно писатьc.Engine.Start(). Продвижение работает только при встраивании без имени поля.
Итоги
- Встраивание структуры без имени поля продвигает её методы и поля на уровень внешней структуры — обращаться к ним можно напрямую.
- Это композиция, а не наследование: нет виртуальных вызовов и переопределения, работает только затенение по имени.
- При совпадении имён метод внешней структуры побеждает; до версии встроенного типа можно достучаться явно через путь
d.Base.Method(). - Если встроенный тип реализует интерфейс, внешняя структура автоматически начинает реализовывать его тоже.