В пакете пытаются объявить метод для типа, импортированного из другого пакета. Какое правило Go делает такое объявление недопустимым?
Метод в Go можно объявить только для локального именованного типа, определённого в том же пакете. Поэтому импортированный тип нельзя расширить новым методом: его набор методов принадлежит пакету, где этот тип объявлен.
Допустимый получатель имеет форму T или *T, где T — локальный именованный тип, не являющийся указателем или интерфейсом.
Методы в Go являются частью набора методов типа, а не отдельными глобальными функциями, связанными с типом только по имени. Ограничение на пакет-владелец сохраняет однозначность набора методов и не позволяет стороннему пакету незаметно менять поведение чужого типа.
В результате автор типа контролирует его методы, а реализация интерфейса остаётся проверяемой по исходному коду пакета, который предоставляет тип.
Представим, что пакет использует тип из внешней библиотеки и хочет добавить ему удобный метод. Прямое объявление такого метода компилятор отклонит, даже если исходный тип экспортирован.
Это важно для интерфейсов: добавление метода к чужому типу могло бы неожиданно изменить его соответствие интерфейсам и поведение клиентского кода. Поэтому для адаптации внешнего типа требуется создать собственный тип-обёртку или новый именованный тип.
Новый именованный тип на основе внешнего типа уже принадлежит текущему пакету, поэтому методы для него объявлять можно. Однако это другой тип: автоматическое присваивание между ним и исходным типом обычно невозможно без явного преобразования, а методы исходного типа не становятся его методами автоматически.
Псевдоним типа не решает проблему. Псевдоним не создаёт новый тип, а лишь даёт другое имя уже существующему, поэтому псевдоним внешнего типа не превращает его в локальный тип.
Для сохранения исходного поведения часто применяют встраивание. Встроенное поле позволяет продвигать методы внешнего типа, а собственные методы обёртки могут добавлять адаптацию:
LocalURL — новый локальный тип, поэтому метод Scheme допустим. WrappedURL — отдельная структура со встроенным указателем на внешний тип; его доступные методы могут быть продвинуты, но поведение и правила работы с nil у обёртки нужно проектировать отдельно.
Выбор между новым типом и обёрткой зависит от задачи. Новый тип даёт более строгую границу API и позволяет скрыть исходные методы, но требует преобразований. Встраивание лучше сохраняет исходный интерфейс, однако может открыть больше поведения, чем нужно, и создать неоднозначности при конфликте имён.
Сервис использует внешний тип конфигурации и хочет добавить проверку Validate. Попытка объявить метод непосредственно для внешнего типа не компилируется.
Можно написать отдельную функцию Validate, не меняя тип. Это самый простой вариант, но функция не войдёт в набор методов и не позволит типу удовлетворять интерфейсу с методом Validate.
Можно создать новый именованный тип. Это обеспечивает собственный набор методов и строгий API, но потребует явного преобразования при передаче значения библиотеке. Можно также создать структуру-обёртку с полем внешнего типа: она лучше отделяет внутреннее представление и может скрыть детали библиотеки, но требует явного делегирования нужных операций.
Для публичного API обычно выбирают обёртку, если внешний тип должен остаться деталью реализации. Если требуется только локальная адаптация без сокрытия исходного представления, подходит новый именованный тип.
Нет. Псевдоним не создаёт новый именованный тип, а обозначает исходный тип. Если исходный тип определён в другом пакете, требование локальности не выполнено.
Нет. Новый тип имеет собственный набор методов, даже если его базовый тип имеет ту же структуру или тот же underlying type. Методы нужно реализовать заново либо использовать встраивание, если оно подходит по семантике.
Да. Обычная функция может принимать внешний тип, потому что для неё нет ограничения на пакет, в котором тип определён. Но такая функция не добавляет метод к типу и не делает его удовлетворяющим интерфейсу, требующему соответствующий метод.