Найти в Дзене
Дневник gophera

Объяснение интерфейсов в Go

Последние несколько месяцев я проводил опрос, в котором спрашивал людей, что им сложно в изучении Go? И постоянно возникает вопрос, - это концепция интерфейсов. Я понимаю. Go был первым языком, который я использовал, у которого были интерфейсы, и я понимаю, что в, то время вся концепция казалась довольно запутанной. Поэтому в этом уроке я не буду помогать кому-либо еще в таком же положении и сделаю несколько вещей: Что такое интерфейсы? Тип интерфейса в Go - это что-то вроде определения. Он определяет и описывает точные методы, которые должны быть у другого типа. Один из примеров типа интерфейсов из стандартной библиотеки является интерфейс fmt.Stringer, который выглядит следующим образом: Мы говорим, что что-то удовлетворяет этому интерфейсу (или реализует этот интерфейс), если у него есть метод с точной сигнатурой String() string. Например, следующий тип Book удовлетворяет интерфейсу, поскольку имеет строковый метод String(): На самом деле не важно, что представляет собой этот тип B
Оглавление

Последние несколько месяцев я проводил опрос, в котором спрашивал людей, что им сложно в изучении Go? И постоянно возникает вопрос, - это концепция интерфейсов.

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

  1. Объясню простым языком, что такое интерфейсы;
  2. Объясню, чем они полезны и как их можно использовать в вашем коде;
  3. Поговорим, что такое интерфейс {} (пустой интерфейс);
  4. И познакомимся с некоторыми полезными типами интерфейсов, которые вы найдете в стандартной библиотеке.

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

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

Один из примеров типа интерфейсов из стандартной библиотеки является интерфейс fmt.Stringer, который выглядит следующим образом:

-2

Мы говорим, что что-то удовлетворяет этому интерфейсу (или реализует этот интерфейс), если у него есть метод с точной сигнатурой String() string.

Например, следующий тип Book удовлетворяет интерфейсу, поскольку имеет строковый метод String():

-3

На самом деле не важно, что представляет собой этот тип Book или, что он делает. Единственное, что имеет значение, это наличие метода String(), который возвращает строковое значение.

Или, в качестве другого примера, следующий тип Count также удовлетворяет интерфейсу fmt.Stringer - опять же, потому что у него есть метод с точной сигнатурой String() string.

-4

Важно понять, что у нас есть два разных типа, Book и Count, которые делают разные вещи. Но их объединяет то, что они оба удовлетворяют интерфейсу fmt.Stringer.

Вы можете думать об этом и наоборот. Если вы знаете, что объект удовлетворяет интерфейсу fmt.Stringer, вы можете положиться на то, что у него есть метод с точной сигнатурой String() string, который вы можете вызвать.

Теперь о важной части.

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

Например, предположим, что у вас есть следующая функция:

-5

Поскольку эта функция WriteLog() использует тип интерфейса fmt.Stringer в объявлении своего параметра, мы можем передать любой объект, который удовлетворяет интерфейсу fmt.Stringer. Например, мы могли бы передать любой из созданных ранее типов Book и Count в метод WriteLog(), и код работал бы нормально.

Кроме того, поскольку передаваемый объект удовлетворяет интерфейсу fmt.Stringer, мы знаем, что он имеет строковой метод String(), который может безопасно вызывать функция WriteLog().

Давайте объединим это в пример, который дает нам представление о силе интерфейсов.

-6

Это довольно круто. Мы создали разные типы Book и Count, но передали их в одну и ту же WriteLog(). В свою очередь, это вызывает их соответствующие функции String() и записывает результат.

Если вы запустите код, вы должны получить вывод, который выгладит следующим образом:

-7

Я не хочу долго останавливаться на этом вопросе. Но ключевой момент, который следует запомнить, заключается в том, что используя тип интерфейса в объявлении нашей функции WriteLog(), мы сделали функцию независимой (или гибкой) от точного типа объекта, который она получает. Важно только то, какие у него есть методы.

Чем они полезны?

Существует множество причин, по которым вы можете в конечном итоге использовать интерфейс в Go, но, по моему опыту, наиболее распространенными являются три:

  1. Чтобы помочь уменьшить дублирование шаблонного кода
  2. Упростить использование mock-объектов в unit тестах
  3. В качестве архитектурного инструмента, помогающего обеспечить разделение между частями вашей кодовой базы.

Давайте пройдемся по этим трем вариантам использования и рассмотрим их более подробно.

Уменьшение шаблонного кода

Хорошо, представьте, что у нас есть структура Customer, содержащая некоторые данные о клиенте. В одной части нашей кодовой базы мы хотим записать информацию о клиенте в bytes.Buffer, а в другой части нашей кодовой базы мы хотим записать информацию о клиенте в os.File на диске. Но в обоих случаях мы хотим с начала сериализовать структуру клиента в JSON.

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

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

-8

И мы можем использовать тот факт, что bytes.Buffer, и тип os.File удовлетворяют этому интерфейсу, потому что у них есть методы bytes.Buffer.Write() и os.File.Write() соответственно.

Давайте взглянем на простую реализацию:

-9
-10

Конечно, это всего лишь игрушечный пример ( и есть другие способы структурировать код для достижения того же конечного результата). Но это прекрасно иллюстрирует преимущества использования интерфейса - мы можем создать метод Customer.WriteJSON() один раз, и мы можем вызывать этот метод в любое время, когда мы захотим записать что-то, удовлетворяющее интерфейсу io.Write.

Но если вы новичок в Go, все ровно возникает пара вопросов: откуда вы знаете, что интерфейс io.Writer вообще существует? И как вы заранее знаете, что bytes.Buffer и os.File оба удовлетворяют этому?

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

Но даже если вы не используете интерфейсы из стандартной библиотеки, никто не мешает вам создавать и использовать свои типы интерфейсов. Далее мы расскажем, как это сделать.

Модульное тестирование и mocking

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

Допустим вы управляете магазином и храните информацию о количестве покупателей и продаж в базе данных PostgreSQL. Вы хотите написать код, вычисляющий уровень продаж (т.е. объем продаж на одного клиента) за последние 24 часа с округлением до 2 знаков после запятой.

Минимальная реализация кода для этого может выглядеть примерно так:

-11
-12
-13

А, что, если мы хотим создать модульный тест для функции calculateSalesRate(), чтобы убедится, что математическая логика в ней работает правильно?

Так что мы можем сделать? Как вы догадались - интерфейсы спешат на помощь!

Решением здесь является создание нашего собственного типа интерфейса, описывающего методы CountSales() и CountCustomers(), на которые опирается функция calculateSalesRate(). Затем мы можем обновить сигнатуру calculateSalesRate(), чтобы использовать этот настраиваемый тип интерфейса в качестве параметра вместо конкретного типа *ShopDB.

Вот так:

-14
-15

После этого на не составит труда создать макет, удовлетворяющий нашему интерфейсу ShopeModel. Затем мы можем использовать этот макет во время модульных тестов, чтобы проверить, правильно ли работает математическая логика в нашей функции calculateSalesRate(). Вот так:

-16

Вы можете запустить этот тест сейчас, все должно работать нормально.

Архитектура приложения

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

Вы можете расширить эту идею, чтобы создавать несвязанные "слои" в более крупных проектах.

Допустим вы создаете веб-приложение, которое взаимодействует с базой данных. Если вы создаете интерфейс, описывающий точные методы взаимодействия с базой данных, вы можете ссылаться на интерфейс во всех обработчиках HTTP вместо конкретного типа. Поскольку обработчик HTTP относится только к интерфейсу, и это помогает разделить уровень HTTP и уровень взаимодействия с базой данных. Это упрощает независимую работу со слоями одного слоя в будущем, не затрагивая другой.

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

Если вы какое-то время программировали на Go, вы, вероятно, сталкивались с пустым типом интерфейса: interface{}. Это может сбивать не много с толку, но я попытаюсь объяснить.

В начале этого поста я сказал:

Тип интерфейса в Go похож на определение. Он определяет и описывает точные методы, которые должен иметь какой-либо другой метод.

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

Или, выражаясь более простым языком, пустой тип интерфейса interface{} похож на подстановочный знак. Везде, где вы видите это в объявлении (например, переменная, параметр функции или поле структуры), вы можете использовать объект любого типа.

Взгляните на следующий код:

-17

В этом фрагменте кода мы инициализируем person map, которая использует строковой тип для ключей и пустой тип интерфейса interface{} для значений. Мы назначили три разных типа в качестве значений map (string, int и float32) - и это нормально. Поскольку объекты любого типа удовлетворяют пустому интерфейсу, код будет работать нормально.

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

-18

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

Например, предположим, что мы хотим получить значение "возраст" и увеличить его на 1. Если вы напишите что-то вроде следующего кода, он не скомпилируется:

-19

И вы получите следующее сообщение об ошибке:

-20

Это происходит потому, что, значение, хранящееся в mape, имеет тип interface{}, а не int, поэтому мы не можем к нему прибавить 1.

Чтобы обойти это необходимо сначала привести к типы int перед прибавлением 1. Вот так:

-21

Если вы сейчас запустите это, все будет работать:

-22

Итак, когда вы должны использовать пустой тип интерфейса в своем собственном коде?

Ответ, вероятно, не так часто. Если вы обнаружите, что тянетесь к нему, сделайте паузу и подумайте, действительно ли использование interface{} является правильным вариантом. Как правило, понятнее, безопаснее и эффективнее использовать конкретные типы или непустые интерфейсные типы. В приведенном выше фрагменте кода было бы более уместно определить структуру Person с соответствующими типизированными полями, подобными этому:

-23

Но при этом пустой интерфейс полезен в ситуациях, когда вам нужно принимать и работать с непредсказуемыми или определяемыми пользователями типами. Именно по этой причине вы увидите, что он используется во многих местах стандартной библиотеки, например в функциях gob.Encode, fmt.Print и template.Execute.

Распространенные и полезные типы

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

Кроме того, в этом списке находится более полный список стандартных библеотек.

Источник: https://www.alexedwards.net/blog/interfaces-explained

#golang #перевод #программирование