Интерфейсы в Golang
Если вы пришли с Java, вы определенно знаете об интерфейсах. Если вы пришли с Python, вы, вероятно, почесываете затылок. Но независимо от того, с какого языка вы пришли, вы будете удивлены тем, как Go реализует интерфейсы.
Интерфейс - это разновидность Go. Но, в отличие от типа структуры, тип интерфейса связан не с состоянием, а с поведением.
Например, структура Dog будет выглядеть так:
type Dog struct {
name string
age int
gender string
isHungry bool
}
С другой стороны, интерфейс Dog будет выглядеть так:
type Dog interface {
barks()
eats()
}
Структура показывает нам некоторые атрибуты Dog, но интерфейс описывает, что эта собака должна делать.
А теперь, допустим, мы пишем заявку о ... ну ... собаках! У нас будет много разных пород. Мы знаем, что Go не поддерживает наследование, как это делают ООП языки, поэтому вместо того, чтобы называть структуру Dog, назовем ее Labrador, и по мере роста нашего приложения мы, вероятно, добавим больше пород.
type Labrador struct {
name string
age int
gender string
isHungry bool
}
Для целей нашего приложения мы хотим разделить этих разных собак на две группы - больших собак и маленьких собак (предположим, мы хотим знать, сколько еды нужно каждой группе). Нам нужна функция, которая добавляет собаку (независимо от породы) в группу:
func addToGroup(d Dog, group []Dog) []Dog {
group = append(group, d)
return group
}
Эта функция по сути является просто оболочкой для встроенной функции добавления, но для простоты предположим, что это какой-то сверхсложный алгоритм.
Обратите внимание, что функция addToGroup
принимает только интерфейс Dog
и часть Dogs. Нигде не упоминается тип лабрадора. Но что, если мы хотим добавить лабрадора по имени Макс в группу под названием «Big dogs»? Как компилятор Go узнает, что Labrador
- это собака?
Компилятор Go будет знать, что тип Labrador
реализует интерфейс Dog
(и, таким образом, получает доступ ко всем функциям, которые принимают интерфейс Dog), только если тип Labrador реализует методы, описанные в интерфейсе Dog.
В Java это немного более ясно. Вы бы напечатали, class Labrador implements Dog
и это уже указывало бы на то, что вы делаете. Вам также придется реализовать методы, но в Golang вы реализуете только методы, и неявно ваша Labrador
становится собакой.
Итак, давайте реализовывать методы barks()
и eats()
, чтобы компилятор Go смог узнать, что такое лабрадор:
func (l Labrador) barks() {
fmt.Println(l.name + " says woof")
}
func (l Labrador) eats() {
if l.isHungry {
fmt.Println(l.name + " is eating. Since he is a labrador, give him xxx brand of food.")
} else {
fmt.Println(l.name + " already ate. Come back later.")
}
}
Я признаю, что это не совсем интеллектуальная бизнес-логика, но, как вы знаете, учебные пособия, как правило, стараются сделать ее максимально простой.
Теперь, когда структура Labrador
реализует оба метода, указанные в интерфейсе Dog
, она имеет доступ ко всем функциям, которые принимают тип Dog
в качестве аргумента.
Давайте посмотрим на вывод в нашем терминале, добавив все это в нашу основную функцию:
func main() {
bigDogs := []Dog{}
max := Labrador{
name: "Max",
age: 5,
gender: "Male",
isHungry: true,
}
max.barks()
max.eats()
fmt.Println("Our group of big dogs:", bigDogs)
bigDogs = addToGroup(max, bigDogs)
fmt.Println("Our group of big dogs now:", bigDogs)
}
Сначала мы создали bigDogs
, в которой мы будем хранить всех наших больших собак. Затем мы создаем лабрадора по имени Макс. Мы вызываем методы barks()
и eats()
просто чтобы увидеть, что все работает.
Это работает, поэтому давайте добавим Макса в нашу группу больших собак. Если бы он был чихуахуа, он, вероятно, не подошел бы.
Как мы видим, функция addToGroup
с радостью принимает Max the Lab, поскольку компилятору Go ясно, что Max - это Lab, а Lab - это собака.
Но это всего лишь игрушечный пример. Где все это на самом деле используется в производственном коде?
Буквально везде.
Например, интерфейсы io.Reader
и io.Writer
используются все время. Всякий раз, когда какой-либо тип используется для чтения некоторых данных (откуда бы он ни был), скорее всего, он реализует интерфейс io.Reader
. Если он где-то пишет (в стандартный формат, в файл и т.д.), скорее всего, он реализует интерфейс io.Writer
.
На самом деле их довольно просто реализовать. Вот интерфейс io.Reader
:
type Writer interface {
Write(p []byte) (n int, err error)
}
Вот и все. Любому типу, которому нужен доступ к функциям, принимающим io.Reader
в качестве аргумента (а их довольно много), просто необходимо реализовать метод Write
. Это не означает, что эта реализация обязательно будет хорошей. Интерфейсы работают как мусор на входе и выходе, поэтому, если вы плохо реализуете метод Write, вы не получите ожидаемого поведения.
Еще одна вещь, которую следует учитывать, заключается в том, что для реализации интерфейса методы должны иметь ту же сигнатуру, что и описанная в интерфейсе. Если ваш метод Write не принимает байтовый фрагмент и не возвращает целое число и ошибку, вы сделали это неправильно.
Также стоит упомянуть, что вы также можете встраивать интерфейсы. Загрузите это:
type ReadWriter interface {
Reader
Writer
}
Интерфейс ReadWriter
реализует как Reader
так и Writer
интерфейсы, то есть любой тип, который хочет получить доступ к функциям, которые принимают ReadWriter
в качестве аргумента, должен реализовать все методы из интерфейсов Reader
и из Writer
(к счастью, всего два метода) .
Наконец, я хочу поговорить о пустом интерфейсе. Допустим, вы не хотите использовать хэш-карту, но хотите использовать различные типы данных для значений карты. Поскольку хэш-карта является статически типизированной, вам необходимо указать какой-либо тип, чтобы вы, вероятно, не могли использовать более одного типа. Вот тут-то и появляется пустой интерфейс.
Пустой интерфейс не реализует никакого поведения, поэтому практически любой тип данных удовлетворяет этому, просто существуя.
Это своего рода хакерский прием, но иногда он может пригодиться и помогает разработчикам, которые переходят со слабо типизированных языков, таких как Python и JavaScript.
Вы бы определили свою карту следующим образом:
maxMap := map[string]interface{}{}
Две фигурные скобки в конце выглядят немного странно, не так ли? Первый принадлежит пустому интерфейсу, а второй создает экземпляр карты.
Для лучшей читабельности вы можете использовать функцию make
:
maxMap := make(map[string]interface{})
Для еще лучшей читабельности вы можете создать свой собственный тип, который в основном будет просто реализовывать пустой интерфейс:
type Any interface{}
maxMap := map[string]Any{}
И теперь мы можем назначать различные типы данных в качестве значений нашей карты без того, чтобы компилятор Go был в плохом настроении:
maxMap["name"] = "some name"
maxMap["age"] = 5
maxMap["gender"] = "Male"
maxMap["isHungry"] = true
fmt.Println(maxMap)
Я лично думал, что вся эта логика интерфейса поначалу была действительно запутанной и излишне сложной (особенно по сравнению с Java, где интерфейсы более понятны), но со временем это действительно имеет смысл.
Хорошо то, что вы, вероятно, не будете писать много собственных интерфейсов, по крайней мере, для большинства повседневных функций.
Но это то, что вам определенно нужно знать, так как оно будет постоянно появляться в коде, который вы используете из модулей стандартной библиотеки. Просто посмотрите на http-модуль. Он переполнен интерфейсами. Понимание их во многом поможет вам в отладке.
Вот и все, ребята, спасибо за чтение!