В чем разница наследования в Go и классического наследования
Что проверяют
Понимаете ли вы, что встраивание в Go это композиция с синтаксическим сахаром, а не наследование под другим именем. Следом почти всегда спрашивают, как тогда в Go получить полиморфизм и что будет, если метод встроенного типа вызывает другой метод, «переопределённый» во внешнем.
Как отвечать
В классическом ООП (Java, C#, C++) наследник является подтипом родителя: объект Dog передаётся туда, где ждут Animal, а виртуальный вызов уходит в реализацию наследника. В Go классов и иерархий нет, есть два независимых механизма:
- встраивание (embedding): поле без имени,
type Dog struct { Animal }. Поля и методыAnimalпродвигаются и доступны какd.Nameиd.Speak(), но это лишь сокращение дляd.Animal.Speak(); - интерфейсы: полиморфизм живёт здесь. Тип реализует интерфейс неявно, просто имея нужные методы, без слова
implements; - затенение вместо переопределения: свой
SpeakуDogскрывает продвинутый. Но методAnimal, внутри которого вызываетсяa.Speak(), всё равно вызовет версиюAnimal: встроенный тип ничего не знает о внешнем, виртуальной диспетчеризации нет; - наборы методов: при встраивании
Tметоды с получателем*Tпопадают только в набор*Dog, при встраивании*Tв оба. Это важно, когда тип должен удовлетворять интерфейсу.
Хорошо показать примеры из стандартной библиотеки: io.ReadWriter собран встраиванием io.Reader и io.Writer, а в структуру часто встраивают интерфейс, чтобы обернуть его и заменить один метод, как в middleware над http.ResponseWriter.
Где спотыкаются
Говорят, что Dog можно передать в функцию, принимающую Animal. Нельзя, это разные типы, компилятор потребует d.Animal.
Ждут шаблонного метода как в Java: базовый метод вызовет «переопределённый» из обёртки. В Go он вызовет свой, и такой код ломается тихо.
Встраивают sync.Mutex в экспортируемую структуру и не замечают, что Lock и Unlock стали частью её публичного API.