При вызове метода через интерфейс какой тип определяет фактически выполняемую реализацию?
Фактически выполняемую реализацию определяет динамический тип значения, хранящегося в интерфейсе. Статический тип интерфейсной переменной задаёт только набор методов, доступных через неё.
Интерфейсы в Go предназначены для отделения кода, использующего поведение, от конкретных типов, это поведение реализующих. Такой подход поддерживает неявное выполнение интерфейсов и позволяет подключать новые реализации без изменения потребляющего кода.
Для этого вызов метода через интерфейс должен находить реализацию во время выполнения, исходя из фактического значения, а не из типа переменной-интерфейса.
Одна интерфейсная переменная может последовательно содержать значения разных типов, если все они реализуют нужный интерфейс. Если ошибочно ориентироваться на статический тип переменной, можно ожидать одну реализацию метода, хотя фактически будет вызван метод типа, помещённого внутрь интерфейса.
Это особенно важно при передаче интерфейсов в функции, использовании полиморфизма и работе с типами, методы которых имеют побочные эффекты или различную семантику.
У интерфейсного значения есть статический тип — тип интерфейса, известный компилятору, — и динамический тип — конкретный тип значения, сохранённого внутри интерфейса. При вызове метода компилятор проверяет доступность метода по статическому типу, но конкретную реализацию выбирает по динамическому типу.
В функции say статический тип параметра — Speaker, поэтому доступен метод Speak. При первом вызове динамический тип интерфейсного значения — Dog, при втором — Cat, поэтому вызываются разные реализации.
Механизм не является выбором по типу переменной, через которую значение передали ранее: после помещения значения в интерфейс важен конкретный динамический тип, сохранённый в этом интерфейсном значении. Если метод имеет указательный получатель, динамическим типом может быть указатель; это влияет на доступность методов и на то, изменяется ли исходное значение.
Вызов через интерфейс обычно удобен для полиморфизма, но имеет компромиссы: интерфейс скрывает конкретный тип, усложняет доступ к его специфичным возможностям и может затруднить трассировку поведения. Если требуется логика, зависящая от конкретного типа, используют type assertion или type switch, но это уменьшает абстракцию и связывает код с конкретными реализациями.
Сервис принимает интерфейс Storage, а в production получает реализацию для базы данных, тогда как в тестах — реализацию в памяти. Вызов метода через Storage автоматически направляется к динамическому типу текущего объекта: код сервиса не меняется при замене хранилища.
Можно было бы передавать конкретный тип базы данных, но это ухудшило бы тестируемость и связало бы сервис с инфраструктурой. Можно было бы использовать type switch внутри сервиса, однако тогда бизнес-логика начала бы знать о каждой реализации и требовала бы изменений при добавлении новой.
Поэтому выбирают небольшой интерфейс Storage и передают разные реализации через него. Результат — замена зависимостей без изменения сервиса, при этом важно проверять, что каждая реализация одинаково соблюдает контракт методов.
Нет, само присваивание интерфейса другому совместимому интерфейсу не меняет динамический тип содержащегося значения. Новый интерфейс может предоставить другой статический набор методов, но вызов общего метода всё равно будет выполнен реализацией исходного динамического типа.
Значение типа T не имеет метода, объявленного только для *T, в своём методном наборе. Поэтому T может не реализовывать интерфейс, тогда как *T его реализует. Если интерфейс содержит *T, вызов метода выполняется через указатель и может изменять объект, на который он указывает.
Интерфейс хранит собственную пару из динамического типа и значения. Если исходную переменную позже переприсвоить другим объектом, ранее полученное интерфейсное значение не изменится и продолжит вызывать метод прежнего динамического типа. Изменение возможно только через состояние общего ссылочного объекта, если оба значения ссылаются на него.