Интерфейсы в Go
Теория: Указатели и интерфейсы
В Go интерфейс описывает поведение. Он задаёт набор методов, которые должен поддерживать тип. Не важно, как устроен этот тип внутри — главное, чтобы у него были нужные методы. Интерфейсы позволяют писать код, который не зависит от конкретной реализации. Если тип поддерживает нужный набор операций, он может быть использован в любом месте, где ожидается такой интерфейс.
Понимание интерфейсов невозможно без понимания того, как Go передаёт данные в функции. По умолчанию всё передаётся по значению — то есть копируется. Когда функция получает аргумент, она работает с его копией. Оригинальное значение остаётся снаружи и не меняется. Если нужно изменить исходные данные, вместо значения нужно передать указатель — ссылку на эти данные. Это касается и обычных функций, и методов структур. А значит, напрямую влияет на то, как именно тип реализует интерфейс и можно ли его передать в интерфейсную переменную.
Простой пример:
А теперь с указателем:
Тот же принцип действует при работе со структурами. Если метод объявлен на значении, он получает копию. Если метод объявлен на указателе, он работает с исходным значением. Go не делает неявного преобразования T в *T, в том числе при передаче в интерфейс.
В обозначениях Go T — сам тип (например, User), а *T — указатель на него (*User). Это разные типы с разными наборами методов. Для интерфейсов важен не «где объявлен метод» сам по себе, а какого типа переменную вы передаёте: именно её набор методов сравнивается с интерфейсом. Если требуемый интерфейсный метод объявлен у *T, интерфейс реализует только *T; значение типа T интерфейс не реализует. Go не «догадывается» взять адрес — всё должно совпадать точно.
Пример:
Так работает:
А так — нет:
Если метод реализован у T, то интерфейс реализуют и T, и *T. Но если метод у *T, то только *T.
Если метод должен изменять поля структуры — он должен быть написан на указателе. В противном случае метод будет работать с копией, и все изменения пропадут после выхода из метода. Указатель позволяет работать с оригиналом.
Если структура небольшая и метод только читает поля, можно использовать значение. Это не даст побочных эффектов, и метод будет доступен и у T, и у *T.
Если метод на указателе участвует в интерфейсе, то в интерфейс всегда нужно передавать указатель. Значение уже не подойдёт — интерфейс не будет считаться реализованным.
Рассмотрим структуру Counter:
Если метод реализован на значении:
То он не изменит оригинал:
Чтобы изменения сохранялись, метод должен быть на указателе:
Теперь работает:
Аналогичная логика с интерфейсом:
Передача i = c приведёт к ошибке компиляции, потому что метод реализован только у *Counter.
Go строго различает тип T и указатель *T. Это два разных типа. Если метод реализован у *T, то T интерфейс не реализует. Если метод реализован у T, то его можно вызывать и через значение, и через указатель. Изменять структуру можно только через методы на указателе. И если интерфейс требует такой метод — в него тоже нужно передавать указатель. Это правило определяет, сработает ли код или упадёт уже на этапе компиляции.


