Разберите ситуацию: тип имеет метод с тем же именем и параметрами, что у интерфейса, но возвращает конкретный тип вместо интерфейсного. Считает ли Go такой тип реализующим интерфейс?
Нет. Go не поддерживает ковариантность возвращаемых типов: для реализации интерфейса сигнатура метода должна полностью совпадать, включая тип возвращаемого значения. Метод, возвращающий конкретный тип, не заменяет метод, возвращающий интерфейс.
Неявная реализация интерфейсов в Go позволяет отделять код, использующий абстракцию, от кода, который её предоставляет. Такой подход уменьшает связанность без необходимости явно объявлять наследование или адаптацию типа.
Чтобы эта модель оставалась предсказуемой, соответствие интерфейсу определяется механически по набору методов. Автоматическое допущение совместимости возвращаемых типов усложнило бы проверку сигнатур и вызов методов через интерфейс.
Предположим, интерфейс требует метод, возвращающий io.Reader, а конкретный тип предоставляет метод с тем же именем, возвращающий *bytes.Buffer. Поскольку *bytes.Buffer реализует io.Reader, может показаться, что такой метод достаточно совместим.
Однако вызывающий код ориентируется на точную сигнатуру интерфейса. Go не вставляет неявное преобразование результата и не меняет тип метода при присваивании значения интерфейсу, поэтому прямое присваивание завершится ошибкой компиляции.
Для реализации интерфейса имя метода, список параметров и список результатов должны совпадать точно. Совпадение методов по имени недостаточно, а отношение «конкретный тип реализует возвращаемый интерфейс» не создаёт совместимость сигнатур.
В примере source сам не реализует Reader: его метод возвращает *bytes.Buffer. adapter явно выполняет роль адаптера и объявляет требуемую интерфейсом сигнатуру, после чего результат конкретного метода можно вернуть как io.Reader.
Это правило относится и к параметрам методов: Go не применяет ковариантность результатов или контравариантность параметров. Если типы отличаются, требуется явный адаптер, обёртка либо изменение самого интерфейса.
В библиотеке хранилища уже существовал тип, метод которого возвращал конкретный буфер. Новый пакет определил интерфейс с таким же методом, но с результатом io.Reader, чтобы скрыть конкретную реализацию.
Рассматривались два варианта. Изменить существующий метод было рискованно: это могло нарушить внешний API и вызвать несовместимость у пользователей. Добавить второй метод с нужной сигнатурой было возможно, но увеличивало публичный API и требовало изменить вызывающий код.
Выбрали небольшой адаптер, который делегировал вызов исходному типу и возвращал тот же результат через io.Reader. Это сохранило обратную совместимость, сделало границу пакетов явной и не создало ложного ожидания автоматической ковариантности в Go.
1. Если конкретный возвращаемый тип реализует интерфейсный, почему Go не может автоматически принять метод?
Потому что реализация интерфейса проверяет сигнатуру метода, а не только возможность присвоить его результат другому типу. Автоматическое преобразование изменило бы контракт метода и потребовало бы дополнительных правил во время вызова. В Go такое преобразование должно быть написано явно в теле адаптирующего метода.
2. Сработает ли это правило, если возвращаемый тип объявлен через псевдоним интерфейсного типа?
Да, если используется именно псевдоним типа, он является другим именем того же типа. Например, псевдоним type Alias = io.Reader не создаёт новый тип, поэтому сигнатура с Alias совпадает с сигнатурой с io.Reader.
Но определённый тип, объявленный через type NewReader io.Reader, уже является отдельным типом. Возвращаемое значение NewReader не делает метод совместимым с методом, возвращающим io.Reader, даже если базовые представления связаны.
3. Может ли обобщённое ограничение сделать возвращаемые типы ковариантными?
Нет. Ограничение параметра типа описывает допустимые типы и операции над ними, но не изменяет правила реализации обычных интерфейсов. Если интерфейс требует точную сигнатуру метода, параметр типа или его ограничение не добавляют автоматическую совместимость возвращаемых типов.
Для обобщённого кода можно написать отдельную функцию или адаптер, который явно преобразует конкретный результат в требуемый интерфейс. Это сохраняет статическую проверку и делает место преобразования видимым.