В языке Go интерфейс — это набор методов, которые могут быть реализованы типом. Иными словами, интерфейс — описание того, что может сделать тип.
Если тип имеет методы, описанные в интерфейсе, то этот тип удовлетворяет интерфейсу.
Синтаксис интерфейса очень простой. Вот пример некоторого интерфейса. Обратите внимание, что интерфейс описываем как тип:
type MyInterface interface { Method1(int) int Method2(a string) string //.... может быть ещё много методов}
В фигурных скобках указывается имя метода, список его аргументов и возвращаемых значений. Названия аргументов могут быть опущены, поэтому достаточно указать тип. Но для лучшего понимания кода их лучше писать: описание метода Create(id int, name string, email string) bool более понятно, чем Create(int, string, string) bool, хотя формально эти методы идентичны.
Интерфейс описывает контракт между различными частями программы. Передавая некоторую переменную в разные части программы, описываем, какие методы ожидаем от этой переменной.
Основное назначение интерфейсов — реализация полиморфизма: с одной стороны, тип может реализовывать несколько интерфейсов в разных контекстах применения, с другой стороны, у нас есть возможность написания алгоритмов, работающих с разными типами данных.
Приведём простой пример. Представим, что у нас есть некоторая структура Person, описывающая человека. В различных областях жизни (и нашей программы) человек может выступать в разных ролях: например, быть студентом, работником, родителем и кем-нибудь ещё. Для конкретной области не имеет значения, кем он является в других. На работе от него ждут работы, в обучении — сдачи домашних работ. Как родитель он может сообщить информацию о детях. А ещё он может сообщать информацию о себе.
Опишем эти отношения в коде:
package person// Person - структура, описывающая человекаtype Person struct { name string homework string children []*Person}// DoHomework — делает домашнюю работу func (p Person) DoHomework() string { return p.homework}// Children — сообщает информацию о детяхfunc (p Person) Children() []*Person { return p.children}// Work — выполняет поручения на работеfunc (p Person) Work( tasks []string ) string { s := p.name + " work:" for _,task := range tasks { s += "\n I do " + task } return s}// String — сообщает информацию о себеfunc (p Person) String() string { return p.name}
Естественно, не требуется одновременное наличие этих методов в разных участках программы. Более того, их вызов может нарушить нормальную работу программы.
Для примера опишем другой пакет, представляющий собой место работы:
package company// Worker — интерфейс работника компании type Worker interface { // всё, что он должен уметь делать, — это работать Work(tasks []string) string}// Company — структура компании type Company struct { // personal — сотрудники компании // обратите внимание, мы создали слайс сотрудников компании, то есть слайс переменных интерфейсного типа Worker personal [] Worker}// Hire — наём нового сотрудника// Сотрудник может быть любого типа: человек, робот или сторожевая собака. Главное, чтобы он умел работать, то есть удовлетворял интерфейсу Worker// Go ещё на этапе компиляции проверяет, соответствует ли интерфейсу переданная переменнаяfunc (с* Company) Hire( newbie Worker ) { с.personal = append(с.personal, newbie) }// Process — работа конкретного сотрудника func (с Company) Process ( id int, tasks []string) (res string) { return c.personal[id].Work(tasks)}
Tip
Обратите внимание, что для Person явно не указывается, что он реализует интерфейс Worker. Снова вспоминаем утиную типизацию: если что-то выглядит как утка, плавает как утка и крякает как утка, то это утка.
В этом и есть суть полиморфизма. company может работать с разными сущностями, единственное требование к которым — уметь работать. Это требование и описывается через интерфейс.
Теперь соединим эти пакеты вместе.
package mainimport ( "company" "person")func main() { pers := person.Person{} comp := company.Company{} comp.Hire(pers) // мы передаём переменную типа Person в функцию, аргументом которой является переменная Worker! }
На этапе компиляции компилятор проверяет, можно ли person присвоить переменной типа Worker. Для этого проверяется, что тип Person имеет все методы интерфейса Worker. В нашем случае они есть, всё работает.
C помощью интерфейсов можно написать код, абстрагированный от внешних модулей: при изменениях в них ничего не нужно переделывать в своём коде, и наоборот.
Интерфейсы добавляют гибкости и снижают связность кода. Пакеты person и company ничего не знают друг о друге, но могут успешно взаимодействовать.
Продолжим рассматривать наш пример.
Предположим, что мы решили добавить в нашу программу роботов, которые могут работать так же, как и люди:
package robotimport "fmt"// Robot — тип роботаtype Robot struct { model string serialId int workCounter int}func (r Robot) String() string { return fmt.Sprintf("Robot %s serialID %d", r.model, r.serialId)}// Work — робот выполняет работы и запоминает количество выполненных задач. Поэтому получатель метода — по указателюfunc (r *Robot) Work(tasks []string) string { res := fmt.Sprintf("%s work:", r) for _, task := range tasks { res += "\n I do " + task } r.workCounter += len(tasks) return res}
Так как тип *Robot реализует интерфейс Worker, то можно устроить робота на работу в компанию.
Tip
С точки зрения Go типы Robot и *Robot (указатель) — разные. В примере метод Work привязан именно к *Robot. Так как формально тип Robot не реализует интерфейс Worker, такой код не скомпилируется:
Поэтому будем использовать указатель на робота. Действительно, в этом есть логика. Раз работа в компании изменяет внутреннее состояние робота, то нужно передать указатель именно на неё.
Такой код скомпилируется нормально. Обратите внимание, что в самой компании ничего не пришлось менять. Мы просто создали роботов, которые удовлетворяют всем её требованиям к сотрудникам. Если бы компания работала со структурами, то нам бы пришлось создавать отдельные методы работы в ней с роботами и с людьми.
Допустим, вашему коду хватит двух методов. Если вы хотите протестировать интеграцию с этой библиотекой, вам в ней ничего не нужно исправлять. В Go можно легко заменить вызовы методов типа на вызовы методов интерфейса:
type Client interface { FetchMessages() ([]Message, error) SendMessage(email string, message string) error}func MyFunc(client Client) { // в параметрах вместо типа *BigAPIClient принимаем интерфейс Client // код функции и вызывающий код остаются без изменений messages, err := client.FetchMessages() if err != nil { // ... } // ...}func main() { client := &BigAPIClient{} MyFunc(client)}
Напишем тестовую заглушку, чтобы протестировать интеграцию.
В коде библиотек можно встретить такую конструкцию:
var _ Client = (*MockClient)(nil)
Эта строчка добавляет явную проверку — реализует ли тип MockClient интерфейс Client. Если данный тип не соответствует спецификации интерфейса, код не скомпилируется. Такая конструкция позволяет сделать проверку до того, как появится код, использующий этот тип.
В Go принято делать интерфейсы по возможности маленькими. Чем проще интерфейс, тем легче воспринимать код. Если в интерфейсе больше 5–10 методов, значит, пора его делить.
Хорошая практика — объявлять интерфейс даже с одним методом. Часто такие интерфейсы называют по имени метода и добавляют суффикс -er.
В итоге интерфейс FileHandle будет содержать три метода: Read, Write и Close.
Композиция интерфейсов — очень важная и удобная в применении вещь. Она позволяет встроить в интерфейс требования другого интерфейса, включая интерфейсы из других пакетов. Например, если компания планирует отправлять всех своих сотрудников на обучение, то она может просто встроить интерфейс Student в интерфейс Worker. Компания может не знать, какие требования задаёт интерфейс Student, но перекладывает ответственность за это на сотрудников.