В структуре Go с встроенным типом: когда методы встроенного типа делают внешнюю структуру реализующей интерфейс?
Внешняя структура реализует интерфейс, если после продвижения методов встроенного типа её методовый набор содержит все методы интерфейса с точными сигнатурами. Для встроенного значения T методы с получателем T доступны внешней структуре и её указателю, а методы с получателем *T — только указателю на внешнюю структуру. При встраивании *T методы T и *T продвигаются и в методовый набор структуры, и в методовый набор её указателя.
Встраивание не является наследованием: оно лишь предоставляет продвигаемые методы и доступ к полю. Если одноимённый метод объявлен самой внешней структурой или возникает неоднозначность, продвижение не даёт ожидаемого метода.
В Go нет наследования реализации классов. Для повторного использования поведения язык использует композицию, в том числе встраивание типов в структуры.
Встраивание позволяет уменьшить шаблонный код делегирования: методы встроенного значения могут быть доступны через внешнюю структуру. При этом сохраняется явная модель интерфейсов: тип удовлетворяет интерфейсу только фактическим методом своим методом или продвинутым методом.
Ошибка часто возникает, когда разработчик видит метод во встроенном типе и предполагает, что интерфейс реализуют все варианты внешней структуры. На практике результат зависит от того, встроено значение или указатель, а также от получателя метода.
Неверная оценка методового набора приводит к ошибкам компиляции при передаче значения в интерфейс. Обратная проблема тоже возможна: внешняя структура может неожиданно начать реализовывать интерфейс после изменения встроенного типа, что меняет доступные перегрузки или выбор метода в коде.
Рассмотрим правило для структуры S, содержащей встроенное значение T:
T продвигается в методовый набор S и *S;*T продвигается только в методовый набор *S.Если же S содержит встроенный указатель *T, методы с получателями T и *T продвигаются в методовые наборы и S, и *S. Поэтому значение структуры с embedded-указателем может удовлетворять интерфейсу, требующему метод с указателем-получателем.
В примере ValueOuter не реализует Named, но *ValueOuter реализует: метод Name имеет получатель *Base и продвигается только к указателю внешней структуры. Значение PointerOuter реализует Named, потому что встроен именно *Base.
Метод интерфейса должен совпадать по имени и сигнатуре. Продвижение не меняет сигнатуру и не создаёт адаптеров. Если внешняя структура объявляет метод с тем же именем, он скрывает продвинутый метод; если методы с одинаковым именем приходят из нескольких встроенных полей на одном уровне, обращение становится неоднозначным и такой метод не считается доступным через продвижение.
Встраивание также влияет на публичный контракт типа. Публичная структура может удовлетворить интерфейсу не из-за явно объявленных методов, а из-за методов встроенного типа. Поэтому при проектировании API полезно явно проверять намеренную реализацию интерфейсов и учитывать изменения методовых наборов встроенных типов.
В библиотеке есть структура Handler, в которую встроен базовый тип с методом Close. Разработчик передаёт Handler в компонент, принимающий интерфейс io.Closer, но получает ошибку компиляции: базовый метод имеет указательный получатель, поэтому значение Handler его не получает.
Вариант с изменением получателя базового метода на значение может сделать доступными методы для большего числа значений, но он неприменим, если метод должен изменять состояние или защищать работу с указателем. Вариант с явным методом-делегатом во внешней структуре делает контракт очевидным, но добавляет шаблонный код.
Практичное решение — передавать *Handler, если состояние изменяемое и базовый метод использует указатель. Дополнительно стоит добавить compile-time-проверку намерения: внешняя структура должна явно документировать, какой именно вариант — значение или указатель — предназначен для интерфейсного использования. Это предотвращает случайные изменения контракта при последующем рефакторинге.
Да, методы могут продвигаться от нескольких встроенных полей. Если имена различаются, они входят в доступный набор независимо. Если одинаковый метод находится на одном уровне вложенности у нескольких полей, обращение к нему неоднозначно, и он не продвигается как однозначный метод внешнего типа. Явный метод внешней структуры устраняет неоднозначность и скрывает продвинутые варианты.
nil, но внешняя структура формально реализует интерфейс?Реализация интерфейса определяется статически методовым набором и не зависит от текущего значения поля. Поэтому внешнее значение может быть передано в интерфейс даже при nil встроенном указателе. Но вызов продвинутого метода обычно вызывает панику при обращении к nil-встроенному полю; это отличается от прямого вызова метода на nil-указателе, когда сам метод потенциально может обработать nil-получатель.
Возможна новая реализация: если добавленный метод подходит по сигнатуре, он может начать продвигаться во внешнюю структуру и удовлетворить ранее не реализуемый интерфейс. Это способно изменить выбор функций или методов, принимающих интерфейсы, а также создать неоднозначность с методом другого встроенного типа. Поэтому публичные встроенные типы следует рассматривать как часть поверхности API, а интерфейсные проверки — закреплять тестами или compile-time-утверждениями.