В языке Go интерфейс — это набор методов, которые могут быть реализованы типом. Иными словами, интерфейс — описание того, что может сделать тип.
Если тип имеет методы, описанные в интерфейсе, то этот тип удовлетворяет интерфейсу.
Синтаксис интерфейса очень простой. Вот пример некоторого интерфейса. Обратите внимание, что интерфейс описываем как тип:
В фигурных скобках указывается имя метода, список его аргументов и возвращаемых значений. Названия аргументов могут быть опущены, поэтому достаточно указать тип. Но для лучшего понимания кода их лучше писать: описание метода Create(id int, name string, email string) bool
более понятно, чем Create(int, string, string) bool
, хотя формально эти методы идентичны.
Интерфейс описывает контракт между различными частями программы. Передавая некоторую переменную в разные части программы, описываем, какие методы ожидаем от этой переменной.
Основное назначение интерфейсов — реализация полиморфизма: с одной стороны, тип может реализовывать несколько интерфейсов в разных контекстах применения, с другой стороны, у нас есть возможность написания алгоритмов, работающих с разными типами данных.
Приведём простой пример. Представим, что у нас есть некоторая структура Person
, описывающая человека. В различных областях жизни (и нашей программы) человек может выступать в разных ролях: например, быть студентом, работником, родителем и кем-нибудь ещё. Для конкретной области не имеет значения, кем он является в других. На работе от него ждут работы, в обучении — сдачи домашних работ. Как родитель он может сообщить информацию о детях. А ещё он может сообщать информацию о себе.
Опишем эти отношения в коде:
Естественно, не требуется одновременное наличие этих методов в разных участках программы. Более того, их вызов может нарушить нормальную работу программы.
Для примера опишем другой пакет, представляющий собой место работы:
Tip
Обратите внимание, что для
Person
явно не указывается, что он реализует интерфейсWorker
. Снова вспоминаем утиную типизацию: если что-то выглядит как утка, плавает как утка и крякает как утка, то это утка.
В этом и есть суть полиморфизма. company
может работать с разными сущностями, единственное требование к которым — уметь работать. Это требование и описывается через интерфейс.
Теперь соединим эти пакеты вместе.
На этапе компиляции компилятор проверяет, можно ли person
присвоить переменной типа Worker
. Для этого проверяется, что тип Person
имеет все методы интерфейса Worker
. В нашем случае они есть, всё работает.
C помощью интерфейсов можно написать код, абстрагированный от внешних модулей: при изменениях в них ничего не нужно переделывать в своём коде, и наоборот.
Интерфейсы добавляют гибкости и снижают связность кода. Пакеты person
и company
ничего не знают друг о друге, но могут успешно взаимодействовать.
Продолжим рассматривать наш пример.
Предположим, что мы решили добавить в нашу программу роботов, которые могут работать так же, как и люди:
Так как тип *Robot
реализует интерфейс Worker
, то можно устроить робота на работу в компанию.
Tip
С точки зрения Go типы
Robot
и*Robot
(указатель) — разные. В примере методWork
привязан именно к*Robot
. Так как формально типRobot
не реализует интерфейсWorker
, такой код не скомпилируется:
Поэтому будем использовать указатель на робота. Действительно, в этом есть логика. Раз работа в компании изменяет внутреннее состояние робота, то нужно передать указатель именно на неё.
Такой код скомпилируется нормально. Обратите внимание, что в самой компании ничего не пришлось менять. Мы просто создали роботов, которые удовлетворяют всем её требованиям к сотрудникам. Если бы компания работала со структурами, то нам бы пришлось создавать отдельные методы работы в ней с роботами и с людьми.
Интерфейсы и код внешних библиотек
Теперь, когда вы знаете синтаксис описания и реализации интерфейсов, рассмотрим практики их использования.
Раз в Go не нужно явно указывать, что тип реализует интерфейс, можно писать свои интерфейсы и к библиотечному коду.
Допустим, вы пользуетесь готовой библиотекой, которая посылает сетевые запросы к API:
Допустим, вашему коду хватит двух методов. Если вы хотите протестировать интеграцию с этой библиотекой, вам в ней ничего не нужно исправлять. В Go можно легко заменить вызовы методов типа на вызовы методов интерфейса:
Напишем тестовую заглушку, чтобы протестировать интеграцию.
Теперь функция одинаково работает как c библиотечными методами, так и с подменёнными тестовыми методами:
Note
В коде библиотек можно встретить такую конструкцию:
Эта строчка добавляет явную проверку — реализует ли тип
MockClient
интерфейсClient
. Если данный тип не соответствует спецификации интерфейса, код не скомпилируется. Такая конструкция позволяет сделать проверку до того, как появится код, использующий этот тип.
Интерфейсы должны быть компактными
В Go принято делать интерфейсы по возможности маленькими. Чем проще интерфейс, тем легче воспринимать код. Если в интерфейсе больше 5–10 методов, значит, пора его делить.
Хорошая практика — объявлять интерфейс даже с одним методом. Часто такие интерфейсы называют по имени метода и добавляют суффикс -er
.
Композиция интерфейсов
В описании интерфейса можно не только перечислять методы, но и встраивать уже существующие интерфейсы — их можно комбинировать:
В итоге интерфейс FileHandle
будет содержать три метода: Read
, Write
и Close
.
Композиция интерфейсов — очень важная и удобная в применении вещь. Она позволяет встроить в интерфейс требования другого интерфейса, включая интерфейсы из других пакетов. Например, если компания планирует отправлять всех своих сотрудников на обучение, то она может просто встроить интерфейс Student
в интерфейс Worker
. Компания может не знать, какие требования задаёт интерфейс Student
, но перекладывает ответственность за это на сотрудников.
Ключевые мысли
- Интерфейс — это тип языка Go, который описывает не структуру переменной, а её поведение.
- Реализация интерфейса — это создание такого типа, который реализует поведение, описанное интерфейсом.
- Переменной типа интерфейс может быть присвоен объект любого типа, если он удовлетворяет этому интерфейсу.
- С точки зрения языка типы
T
и*T
— разные. - Интерфейс описывается в том же пакете, в котором применяется, и является частью его контракта для внешних пакетов, которые его реализуют.
- Интерфейсы позволяют снизить связность кода.
- Интерфейсы позволяют реализовать полиморфизм и сокрытие в парадигме ООП.
- Интерфейсы можно комбинировать.
- Можно писать свои интерфейсы и к библиотечному коду.
📂 Go | Последнее изменение: 26.08.2024 19:59