Когда я только начал программировать на Go, мне было сложно понять интерфейсы Go. После многих часов чтения и исследований я написал эту статью как попытку упростить интерфейсы Go для начинающих Gophers.

В этом руководстве я стремлюсь ответить на следующие вопросы с помощью простого, простого английского и детских примеров.

  1. Что такое интерфейс Go? Как ты это используешь?
  2. Чем полезен интерфейс Go?
  3. Как интерфейсы работают под капотом?

Я предполагаю, что вы знакомы с базовыми синтаксисами Go, такими как функции и структуры. Давайте начнем! 🏃

Что такое интерфейс Go?

Формально интерфейс Go - это группа сигнатур методов, которую тип может реализовать. Смущенный? Чтобы лучше понять, давайте разберем это утверждение на три вопроса.

🤔 ️Что такое тип?

Тип относится к типу данных. В Go каждая переменная, параметры функции и возвращаемые значения должны иметь тип. Go использует минималистичную систему шрифтов, с которой очень легко работать.

Мы можем добавить поведение к типу данных Go, прикрепив к нему функции. Функции, относящиеся к типу, называются методами. Чтобы продемонстрировать эту идею, давайте сначала определим структуру Donald и две функции Quack и Walk.

package main
import "fmt"
type Donald struct {}
func Quack() {
    fmt.Println("I am Donald Duck!")
}
func Walk() {
    fmt.Println("I waddle")
}
func main() {
    Quack() // I am Donald Duck!
    Walk() // I waddle
}

В этом (скучном) примере Quack и Walk - обычные функции Go, не связанные с Donald. Чтобы сделать их методами Donald, мы обновляем код следующим образом:

package main
import "fmt"
type Donald struct {}
// Make Quack a method of Donald
func (d Donald) Quack() {
    fmt.Println("I am Donald Duck!")
}
// Make Walk a method of Donald
func (d Donald) Walk() {
    fmt.Println("I waddle")
}
func main() {
    d:= new(Donald)
    d.Quack()
    d.Walk()
}

Обратите внимание, как мы добавили (d Donald) перед Quack и Walk. Это превращает функции в методы Donald. Чтобы использовать методы, нам нужно создать экземпляр Donald и вызвать их с помощью оператора точки.

🤔 Что такое подпись метода?

Подпись метода - это идентификатор метода, как и собственноручная подпись человека. В Go сигнатура метода состоит из имени метода, возвращаемого типа и его параметров.

Например, метод Quack выше, который не принимает аргументов и ничего не возвращает, имеет следующую сигнатуру метода:

Quack()

Метод Walk имеет аналогичную сигнатуру с другим именем метода.

Walk()

Важно отметить, что сигнатура метода не имеет тела метода. Другими словами, он ничего не говорит нам о базовой реализации метода.

Теперь мы наконец можем создать наш первый интерфейс Go! Помните, что интерфейс - это просто набор сигнатур методов. Назовем наш интерфейс Duck подписями Quack и Walk.

type Duck interface {
    Quack()
    Walk()
}

Это все хорошо, но интерфейс сейчас ничего не делает. Как мы его используем? Как тип реализует интерфейс?

🤔 Как реализовать интерфейс?

В Go говорят, что тип данных реализует интерфейс, если у него есть методы, удовлетворяющие сигнатурам методов этого интерфейса.

Другими словами, когда тип обеспечивает реализацию методов, определенных в интерфейсе, вместе с теми же параметрами и возвращаемыми типами, он реализовал интерфейс.

Например, структура Donald реализует интерфейс Duck, потому что у нее есть два метода Quack и Walk, которые не принимают аргументов и ничего не возвращают.

💻 Язык Go: Дональд реализует интерфейс Duck.

🧍 Человеческий язык: Дональд - утка!

type Duck interface {
    Quack()
    Walk()
}
// Donald implements the Duck interface
type Donald struct {}
func (d Donald) Quack() {
    fmt.Println("I am Donald Duck!")
}
func (d Donald) Walk() {
    fmt.Println("I waddle")
}

Важно помнить, что тип реализует интерфейс неявно. Никакого дополнительного синтаксиса или ключевых слов не требуется. Единственное условие - тип должен иметь методы интерфейса.

Зачем использовать интерфейсы Go?

В предыдущем разделе я объяснил, что такое интерфейс Go и необходимый синтаксис для его использования. Напомним, я создал структуру Donald, которая реализует интерфейс Duck двумя методами, Quack и Walk.

Чтобы эффективно использовать интерфейсы Go, нам нужно понимать, почему и как интерфейс может быть полезен в нашей программе. По сути, интерфейсы Go позволяют нам выполнять так называемую утиную печать.

💡 Утиная печать определяет объект или тип с помощью ЧТО ЭТО МОЖЕТ СДЕЛАТЬ вместо того, чем они являются на самом деле.

Как обычно, подобные концепции можно прояснить на примерах. Давайте определим новую структуру под названием Thor, основанную на моем любимом персонаже Marvel!

type Thor struct {}

Для тех, кто не знает, кто такой Тор, он - бог грома в кинематографической вселенной Marvel и в скандинавской мифологии.

Как видите, он ни в коей мере, ни по форме, ни по форме не утка. Но благодаря интерфейсам Go, мы можем сделать из него утку! Все, что нам нужно сделать, это заставить Thor реализовать Duck интерфейс, предоставив ему методы Quack и Walk!

// Thor implements the Duck interface
type Thor struct {}
func (t Thor) Quack() {
    fmt.Println("I am the God of Thunder!")
}
func (t Thor) Walk() {
    fmt.Println("I fly and shoot lightning")
}

Поскольку Тор теперь может крякать и ходить как утка, с ним можно обращаться как с уткой! Это основная идея утиной печати: объект определяется тем, что он может делать (методы, которые он имеет), а не тем, что он есть (его тип).

🦆 Если он ходит как утка и крякает как утка, то это, должно быть, утка!

Хорошо ... Итак, я могу сделать Тора уткой с помощью интерфейсов Go. Как это помогает мне при написании программ Go? Представим, что вы работаете над приложением, которое заставляет объекты крякать и ходить как утка.

Для начала создайте структуру с именем Donald, как в предыдущем разделе, с помощью методов Quack и Walk. Затем вы создаете обычную функцию, которая принимает аргумент типа Donald и вызывает оба метода.

func behaveLikeDonald(d Donald) {
    d.Quack()
    d.Walk()
}

Люди любят Дональда, и популярность вашего приложения растет! Однажды ваши пользователи захотят, чтобы Тор вел себя как утка. Достаточно просто, вы повторяете то, что делали с Donald.

func behaveLikeThor(t Thor) {
    t.Quack()
    t.Walk()
}

Теперь ваше приложение становится лучше! Все больше людей начинают требовать, чтобы разные предметы вели себя как утки. Внезапно вы понимаете, что ваш код не масштабируется, потому что вам нужно создать новую функцию для каждого объекта!

Разве не было бы замечательно, если бы вы могли создать только одну функцию для обработки любых объектов, которые имеют как Quack, так и Walk метод? Как вы уже догадались, На помощь приходят интерфейсы!

В Go интерфейсы можно рассматривать как тип в параметрах функции и возвращаемых значениях. В приведенном выше примере и Donald, и Thor относятся к типу интерфейса Duck, потому что они реализуют интерфейс (т. Е. Имеют методы Duck).

При этом вам понадобится только одна функция, которая принимает аргумент типа интерфейса Duck. Это упрощает масштабирование вашего приложения с учетом количества объектов, связанных с Duck!

// Define function argument to be of type Duck
func behaveLikeDuck (d Duck) {
    d.Quack()
    d.Walk()
}

Если вы знакомы с объектно-ориентированным программированием (ООП), вы можете сказать, что функция behaveLikeDuck достигла полиморфизма.

В заключение, интерфейсы Go позволяют нам создавать абстракции на основе методов, которые есть у типа ввода вместо его фактического конкретного типа. Это позволяет нам создавать гибкие, расширяемые и масштабируемые абстракции!

Если вы по-прежнему не видите преимуществ интерфейсов Go, не беспокойтесь об этом. Когда вы только начинаете работать с Go, вам редко нужно разрабатывать собственные интерфейсы.

Главный вывод - идея использования интерфейсов в качестве типов данных, поскольку это общий шаблон проектирования абстракций Go. Всегда помните следующее утверждение:

Тип интерфейса касается того, что объект может делать, а не того, что есть на самом деле. Другими словами, утиная печать 🦆.

Под капотом с интерфейсными ценностями

В предыдущем разделе я привел простой пример того, как интерфейсы Go могут быть полезны при разработке мощных и многократно используемых абстракций.

Напомним, я создал специальную структуру Donald, которая реализует Duck интерфейс с двумя методами Quack и Walk. Также существует функция behaveLikeDuck, которая принимает аргумент типа интерфейса Duck.

func behaveLikeDuck (d Duck) {
    d.Quack()
    d.Walk()
}

Давайте подумаем о том, как интерфейсы Go работают под капотом. Чтобы быть конкретным, что происходит, когда мы вызываем функцию behaveLikeDuck?

Когда мы передаем переменную в behaveLikeDuck, Go выполняет проверку статического типа во время компиляции, чтобы проверить, соответствует ли переменная интерфейсу Duck. Если этого не происходит, компиляция не выполняется.

Во время выполнения Go выполняет преобразование типа и создает локальную переменную d типа Duck внутри behaveLikeDuck. Возникает вопрос: когда вызываются методы Quack и Walk, как Go узнает, какую конкретную реализацию методов следует выполнить?

❗ Может быть много типов, реализующих Duck, каждый со своей собственной версией Quack и Walk.

Локальная переменная d - это значение интерфейса. В простейшем смысле вы можете представить себе значение интерфейса как список с двумя частями информации: базовые данные и конкретный тип интерфейса .

Например, если мы предоставляем переменную типа Donald, базовые данные d указывают на экземпляр Donald, а конкретный тип - это структура Donald с ее реализациями методов.

Значение интерфейса также предоставляет нам метод для выполнения утверждений типа с его базовым конкретным типом. Мы не будем сейчас вдаваться в подробности, так как это заслуживает отдельной статьи.

Когда мы вызываем метод для значения интерфейса d, Go ищет метод с тем же именем под конкретным типом и выполняет его. В этом смысле Go достаточно динамичен, чтобы работать с разными, но связанными объектами во время выполнения.

Вы должны помнить одно предостережение: методы, к которым вы можете получить доступ для значения интерфейса, только те, которые определены в интерфейсе.

Вернемся к приведенному выше примеру, если Donald имеет другой метод с именем Eat, вы не можете вызвать его со значением интерфейса d, потому что он не определен в интерфейсе Duck. Опять же, если вы знакомы с ООП, это согласуется с тем, как повышающее преобразование работает в таких языках, как Java.

Последние мысли

На этом мы завершаем введение в интерфейсы Go для начинающих! В этой статье я объяснил, что такое интерфейсы Go, почему они полезны, какова основная реализация интерфейсов Go и утиная типизация.

Об интерфейсах Go можно поговорить еще много, но я оставлю их для своих будущих статей, так как это займет немного времени.

Спасибо за чтение. Мир ✌️!

использованная литература

  1. Джордан Орелли:« Как использовать интерфейсы в Go? »
  2. Русс Кокс« Структуры данных Go: интерфейсы »