Встраивание в одном примере
Поле с типом, но без имени, называется встроенным. Поля и методы встроенного типа становятся доступны прямо на внешнем типе.
Вывод:
Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}
Имя встроенного поля совпадает с именем его типа, User. Так к нему обращаются в литералах (User: User{...}) и когда нужно само внутреннее значение (a.User). Продвинутое поле нельзя задать в литерале по короткому имени: Admin{Name: "Ana"} не компилируется с ошибкой unknown field Name in struct literal of type Admin.
Продвинутые методы удовлетворяют интерфейсам
Продвижение добавляет методы встроенного типа в набор методов внешнего. Поэтому внешний тип удовлетворяет всем интерфейсам, которым удовлетворяет внутренний.
Правила наборов методов определяются встроенным типом. Встраивание T продвигает методы T с получателем-значением и во внешнее значение, и во внешний указатель, а методы *T с получателем-указателем только во внешний указатель. Встраивание *T продвигает оба набора в оба варианта. Если сомневаетесь, встраивайте указатель или используйте внешний тип через указатель.
У встраивания указателя есть цена: указатель может быть nil. Service{} без Logger компилируется, а svc.Log(...) затем паникует.
Встраивание не наследование
Встраивание похоже на создание подкласса, но три вещи работают иначе, чем в Java или Python.
Внешний тип не является внутренним. Admin не является User. Функция, принимающая User, не примет Admin; передайте a.User.
Виртуальной диспетчеризации нет. Продвинутый метод выполняется на встроенном значении и ничего не знает о внешнем типе. Если он вызывает другой метод, то вызывает версию своего типа, даже когда во внешнем типе есть метод с тем же именем.
Вывод:
woof
The animal says ...
В языке с классами Speak напечатал бы «woof». В Go d.Speak() это сокращение для d.Animal.Speak(), и получателем этого вызова является Animal. Если нужно поведение, зависящее от типа, используйте интерфейс: передайте Sounder в код, которому он нужен.
Внутренний тип не знает, что его встроили. super нет. Чтобы расширить продвинутый метод, определите на внешнем типе метод с тем же именем и явно вызовите внутренний:
func (a Admin) Greet() string {
return a.User.Greet() + " (admin)"
}
Конфликты имён и затенение
Поле или метод внешнего типа затеняет продвинутое имя с тем же названием, как Dog.Sound выше. Побеждает менее глубокое имя.
Когда два встроенных типа на одной глубине продвигают одно и то же имя, оно становится неоднозначным. Программа всё равно компилируется, пока никто не использует это имя:
Разрешите конфликт, используя полный путь, или определите ID на внешнем типе.
Встраивание интерфейсов
Интерфейсы могут встраивать другие интерфейсы. Стандартная библиотека так строит интерфейсы побольше:
type ReadWriter interface {
Reader
Writer
}
Структура тоже может встроить интерфейс. Тогда структура удовлетворяет этому интерфейсу через значение, лежащее в поле, а переопределяете вы только нужные методы. Это шаблон декоратора почти без кода:
Явный вызов c.Reader.Read(p) обязателен. Если написать c.Read(p) внутри Read, метод будет вызывать сам себя бесконечно.
Встраивание интерфейса в тестовую заглушку это частый короткий путь: встройте большой интерфейс, реализуйте тот единственный метод, который нужен тесту, а остальные не трогайте. Любой нереализованный метод при вызове паникует с разыменованием nil-указателя, и в тесте часто именно это и нужно.
Частое реальное применение: встраивание sync.Mutex
type Stats struct {
sync.Mutex
hits map[string]int
}
func (s *Stats) Hit(page string) {
s.Lock()
defer s.Unlock()
s.hits[page]++
}
Читается это хорошо, но заодно экспортирует Lock и Unlock как часть API Stats, так что любой вызывающий код может заблокировать вашу структуру. Для типов, которые используются вне пакета, именованное поле (mu sync.Mutex) оставляет блокировку приватной. Этот компромисс касается любого встраивания: всё, что экспортирует встроенный тип, становится частью публичного интерфейса вашего типа.
Частые ошибки
- Ожидание виртуальной диспетчеризации. Продвинутые методы никогда не вызывают переопределения внешнего типа.
- Задание продвинутых полей в литерале. Используйте имя встроенного типа:
Admin{User: User{Name: "Ana"}}. - nil во встроенных указателях. Встроенный
*Tили интерфейс нужно задать до вызова его методов. - Случайное расширение API. Встраивание выставляет на вашем типе все экспортированные методы внутреннего типа.
Часто задаваемые вопросы
Что такое встраивание структур в Go?
Объявление поля только с типом, без имени: type Admin struct { User; Level int }. Поля и методы встроенного User продвигаются, поэтому a.Name и a.Greet() работают прямо на Admin. Встроенное значение остаётся обычным полем, доступным как a.User.
Есть ли в Go наследование?
Нет. В Go нет классов и наследования подтипов. Встраивание даёт повторное использование кода через композицию: внешний тип получает методы внутреннего, но Admin не является User. Нельзя передать Admin туда, где ожидается User, а продвинутые методы не могут вызывать методы внешнего типа. Полиморфизм в Go дают интерфейсы.
Как инициализировать встроенную структуру в Go?
В составном литерале обращайтесь к встроенному полю по имени его типа: Admin{User: User{Name: "Ana"}, Level: 2}. Задать продвинутые поля в литерале напрямую нельзя: Admin{Name: "Ana"} не компилируется с ошибкой unknown field Name in struct literal of type Admin.
Можно ли встроить интерфейс в структуру в Go?
Да. Тогда структура удовлетворяет интерфейсу через встроенное значение, а отдельные методы можно переопределить. Это частый приём для декораторов и тестовых заглушек. Если встроенное поле-интерфейс равно nil, вызов непереопределённого метода вызывает панику разыменования nil-указателя.