При каких условиях значение структуры удовлетворяет интерфейсу только через указатель, а не само по себе?
Значение структуры удовлетворяет интерфейсу только через указатель, если для реализации хотя бы одного метода интерфейса используется указательный получатель. Методический набор типа T содержит только методы с получателем T, а методический набор *T — методы с получателями T и *T.
Поэтому *T может реализовать интерфейс, который не реализует T. Автоматическое взятие адреса при вызове метода для адресуемой переменной не меняет это правило при присваивании значения интерфейсу.
В Go интерфейсы реализуются неявно: типу не нужно явно объявлять, что он поддерживает интерфейс. Чтобы такая модель оставалась проверяемой компилятором, язык определяет реализацию интерфейса через методический набор типа.
Разделение T и *T позволяет учитывать важное различие: метод указателя может изменять исходный объект и не обязан работать с копией значения. При этом методы со значимым получателем могут быть доступны и указателю без дублирования объявлений.
Рассмотрим структуру, у которой один требуемый интерфейсом метод объявлен с указательным получателем. Переменная этой структуры может успешно вызвать такой метод: если переменная адресуемая, компилятор автоматически использует её адрес.
Однако попытка передать саму структуру туда, где ожидается интерфейс, проверяет не возможность вызова метода у конкретной переменной, а методический набор типа T. Если в нём нет нужного метода, присваивание не скомпилируется.
Неверное понимание этого различия приводит к ошибкам при проектировании API, неожиданным несовпадениям с интерфейсами и лишним копированиям структур.
Методы с получателем типа T входят в методический набор T. Они также входят в методический набор *T, поэтому указатель может использовать методы обоих видов.
Методы с получателем *T входят только в методический набор *T. Следовательно, интерфейс реализует *T, но не T, если среди обязательных методов есть хотя бы один такой метод.
Вызов метода и проверка реализации интерфейса — разные операции. Для адресуемой переменной d компилятор может преобразовать d.Read() в обращение через &d, но при передаче d в интерфейс он не меняет тип Document на *Document автоматически.
Это ограничение особенно важно для значений, которые неадресуемы: временных результатов функций, значений, извлечённых из интерфейса, и некоторых других выражений. Кроме того, значение, помещённое в интерфейс, обычно рассматривается как отдельное значение, поэтому не следует рассчитывать на возможность изменить исходную структуру через указательный метод, если в интерфейс попала её копия.
Указательный получатель обычно выбирают, когда метод изменяет состояние структуры, структура велика и копирование нежелательно или тип должен единообразно использоваться через указатели. Значимый получатель уместен для небольших неизменяемых значений, но он копирует структуру при вызове.
В сервисе структура Request должна реализовать интерфейс обработчика, а её методы обновляют внутреннее состояние. Если объявить методы со значимыми получателями, интерфейс смогут реализовать и Request, и *Request, но каждый вызов будет работать с копией и может не изменить исходный объект.
Первый вариант — оставить значимые получатели. Его плюс — более широкий методический набор и удобная работа со значениями; минус — риск незаметного копирования и невозможность корректно изменять состояние.
Второй вариант — использовать указательные получатели и передавать в интерфейс только *Request. Это выбранное решение: оно явно фиксирует владение изменяемым состоянием и предотвращает случайную передачу копии. Цена решения — необходимость следить за указателями и помнить, что Request и *Request имеют разные методические наборы.
T реализует интерфейс, если его переменная адресуемая?Нет. Автоматическое взятие адреса действует при вызове метода у адресуемого выражения, но не расширяет методический набор T. Поэтому адресуемая переменная v может поддерживать вызов v.M(), хотя тип T всё ещё не удовлетворяет интерфейсу с методом M, объявленным для *T.
Обычно нет, если извлечённое значение неадресуемо. Интерфейс хранит динамическое значение типа T, а не автоматически созданный *T; компилятор не может произвольно изменить тип этого значения и получить адрес исходного объекта. Для вызова указательного метода нужно заранее иметь указатель или адресуемую переменную подходящего типа.
Это сохраняет единый методический набор для *T и предотвращает случайное копирование структуры в отдельных вызовах. Такой подход полезен для больших структур, типов с внутренним состоянием и типов, которые должны реализовывать интерфейсы только через указатель. Компромисс — значения T нельзя напрямую передавать как реализацию этих интерфейсов, поэтому API становится менее удобным для работы с копиями.