Перейти к содержимому
Теория · Структуры и методы · урок 8 из 11

Встраивание

Представьте анкету сотрудника, в которую целиком вклеена личная анкета: имя, фамилия и всё остальное. Поверх неё в анкете сотрудника есть ещё графа «Компания». Отдел кадров читает имя прямо с листа сотрудника и не ищет сначала вклеенную страницу.

Так и в Go. Тип Employee должен уметь всё, что умеет Person, и хранить ещё название компании. Если положить Person в обычное поле p Person, каждое обращение растягивается: e.p.Name, e.p.Greet(). Код работает, но имя p в таких обращениях ничего не добавляет. В Go тип можно встроить в структуру, и тогда его поля и методы доступны напрямую:

GO
package main

import "fmt"

type Person struct{ Name string }
type Employee struct {
    Person
    Company string
}

func (p Person) Greet() string { return "Hi, " + p.Name }
func main() {
    e := Employee{Person: Person{Name: "Ann"}, Company: "Abyss"}
    fmt.Println(e.Name, e.Greet()) // Ann Hi, Ann
}

Давайте разберём пример по строкам:

  1. В Employee первым идёт поле без имени — только тип Person.
  2. Литерал заполняет вклеенную анкету значением Person{Name: "Ann"}, а графу Company — строкой "Abyss".
  3. e.Name читает имя прямо из e.
  4. e.Greet() вызывает метод Person, и программа печатает Ann Hi, Ann.

Встроенное поле

Поле, у которого указан только тип, без имени, называют встроенным, а сам приём — встраиванием. Поля и методы встроенного типа продвигаются во внешнюю структуру:

  • поле: e.Name означает e.Person.Name;
  • метод с получателем-значением: e.Greet() означает e.Person.Greet();
  • метод с получателем-указателем: если бы Greet принимал *Person, вызов e.Greet() у переменной e тоже бы работал.

Встроенное поле — часть структуры, как любое другое поле. Имя у него тоже есть, и оно совпадает с именем его типа. Отсюда два следствия:

  • в литерале пишут Person: Person{Name: "Ann"};
  • значение Person целиком достают как e.Person.

Если встроенное поле не указать в литерале, оно получит нулевое значение: в Employee{Company: "Abyss"} поле Name пустое.

Затенение имён

Своё поле внешней структуры с тем же именем затеняет продвинутое. Так же свой метод Greet закрыл бы продвинутый. Встроенное поле при этом остаётся доступным через имя типа:

GO
type Manager struct {
    Person
    Name string
}
GO
m := Manager{Person: Person{Name: "Ann"}, Name: "Boss"}
fmt.Println(m.Name, m.Person.Name, m.Greet()) // Boss Ann Hi, Ann

Давайте разберём, что печатает вторая строка:

  1. m.Name — своё поле Manager, поэтому Boss.
  2. m.Person.Name — поле встроенного Person, поэтому Ann.
  3. m.Greet() возвращает Hi, Ann: метод Greet объявлен у Person и видит только поля Person.

Метод Greet не знает о поле Name типа Manager. Вклеенная анкета не догадывается, что лежит внутри другой.

Это не наследование

В языках с наследованием тип-наследник сам считается базовым типом и подставляется вместо него. Встраивание так не работает: Employee содержит значение Person, но сам типом Person не становится. Метод встроенного типа всегда работает со своим значением и никогда не вызывает методы внешнего типа.

Этот вопрос часто задают на собеседовании: «Встраивание в Go — это наследование?» Ответ — нет:

  • внешняя структура содержит внутреннюю как обычное поле;
  • поля и методы внутренней доступны по короткому пути;
  • Employee не становится типом Person и не подставляется вместо него.

Здесь кончается и картинка с анкетой: отдел кадров примет лист сотрудника за личную анкету, а компилятор — нет.

Частая ошибка

Employee передают туда, где ждут Person:

GO
func greet(p Person) string { return "Hello, " + p.Name }

func main() {
    e := Employee{Person: Person{Name: "Ann"}, Company: "Abyss"}
    fmt.Println(greet(e)) // ✗
}
TEXT
cannot use e (variable of struct type Employee) as Person value in argument to greet

Программа не компилируется: Employee и Person — разные типы, даже если один встроен в другой. Передайте встроенное поле явно: greet(e.Person).

Главное

  • Встроенное поле — поле без имени, только тип; его поля и методы продвигаются во внешнюю структуру.
  • Своё поле или метод с тем же именем затеняет продвинутый, а встроенное значение доступно как e.Person.
  • Встраивание — не наследование: Employee не становится типом Person и не передаётся вместо него.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом