При присваивании одного интерфейсного значения другому какой динамический тип сохраняется в целевом интерфейсе?
В целевом интерфейсе сохраняется тот же динамический тип и то же динамическое значение, которые находились в исходном интерфейсе. Меняется только статический интерфейсный тип переменной, поэтому набор доступных через неё методов может стать уже.
Интерфейсы в Go отделяют контракт от конкретной реализации и позволяют передавать объект между слоями с разными уровнями абстракции. Присваивание одного интерфейса другому поддерживает такую замену представления без создания специальной обёртки вокруг исходного значения.
Важно различать статический тип переменной и динамический тип значения внутри интерфейса. Ошибка в этом различии приводит к неверному ожиданию, что после присваивания объект будет преобразован в новый тип или потеряет свою конкретную реализацию.
Целевой интерфейс должен быть совместим с исходным статическим типом: его набор методов не может быть шире гарантированного исходным интерфейсом. При этом фактический объект внутри интерфейса не меняется.
Интерфейсное значение концептуально содержит пару: динамический тип и динамическое значение. При присваивании интерфейса другому интерфейсу Go проверяет совместимость интерфейсных типов, а затем переносит эту пару в целевой интерфейс.
В dst статический тип — Reader, но динамический тип остаётся file. Поэтому вызов Read направляется к методу file.Read, а не к какому-либо методу-обёртке интерфейса. Интерфейс не создаёт новый объект и не копирует объект в другой конкретный тип.
Изменяется только доступный контракт: через dst нельзя вызвать Log, поскольку этого метода нет в Reader. Если исходный интерфейс имеет более узкий набор методов, обычное присваивание его более широкому интерфейсу не разрешается компилятором; для такой проверки нужен type assertion или другая явная проверка.
Если исходный интерфейс равен nil, целевой интерфейс также будет nil. Если внутри находится типизированный nil, например nil-указатель, интерфейс остаётся ненулевым, поскольку его динамический тип всё ещё записан.
Сервис получает объект через интерфейс LoggedReader, а функция обработки принимает более узкий Reader. Прямое присваивание удобно: обработчик видит только необходимый контракт, но вызовы всё равно исполняются на исходном объекте.
Можно было передать конкретный тип file, однако это связало бы обработчик с реализацией и ухудшило бы заменяемость. Можно было создать адаптер, но он добавил бы лишний объект и мог бы изменить поведение идентичности или хранения состояния. Поэтому присваивание интерфейса более узкому интерфейсу обычно является предпочтительным решением, когда дополнительный контракт обработчику не нужен.
Результат — сохранение исходной реализации при контролируемом уменьшении видимого API. Если позже потребуется Log, его следует запрашивать отдельно через проверку, а не рассчитывать, что узкий интерфейс автоматически предоставит скрытые методы.
Обычное присваивание проверяется с учётом статического типа исходного выражения. Если исходная переменная объявлена как Reader, компилятор не знает, что её текущее динамическое значение может иметь Log, поэтому присваивание в LoggedReader не допускается. Для этого нужна динамическая проверка, например type assertion; при успехе она вернёт значение целевого интерфейса с тем же динамическим объектом.
Нет, он не теряется из динамической части интерфейсного значения. Ограничивается только статический набор операций, доступных через переменную целевого интерфейсного типа. Поэтому последующая проверка динамического значения на другой интерфейс может успешно обнаружить методы, которых нет у текущего статического интерфейса.
Нет. Вызов через целевой интерфейс использует тот же динамический тип и его методовую реализацию. Меняется лишь способ статической проверки вызова: разрешены только методы целевого интерфейса, а диспетчеризация к конкретной реализации остаётся прежней.