Последние несколько месяцев я проводил опрос, в котором спрашивал людей, что им сложно в изучении Go? И постоянно возникает вопрос, - это концепция интерфейсов.
Я понимаю. Go был первым языком, который я использовал, у которого были интерфейсы, и я понимаю, что в, то время вся концепция казалась довольно запутанной. Поэтому в этом уроке я не буду помогать кому-либо еще в таком же положении и сделаю несколько вещей:
- Объясню простым языком, что такое интерфейсы;
- Объясню, чем они полезны и как их можно использовать в вашем коде;
- Поговорим, что такое интерфейс {} (пустой интерфейс);
- И познакомимся с некоторыми полезными типами интерфейсов, которые вы найдете в стандартной библиотеке.
Что такое интерфейсы?
Тип интерфейса в Go - это что-то вроде определения. Он определяет и описывает точные методы, которые должны быть у другого типа.
Один из примеров типа интерфейсов из стандартной библиотеки является интерфейс fmt.Stringer, который выглядит следующим образом:
Мы говорим, что что-то удовлетворяет этому интерфейсу (или реализует этот интерфейс), если у него есть метод с точной сигнатурой String() string.
Например, следующий тип Book удовлетворяет интерфейсу, поскольку имеет строковый метод String():
На самом деле не важно, что представляет собой этот тип Book или, что он делает. Единственное, что имеет значение, это наличие метода String(), который возвращает строковое значение.
Или, в качестве другого примера, следующий тип Count также удовлетворяет интерфейсу fmt.Stringer - опять же, потому что у него есть метод с точной сигнатурой String() string.
Важно понять, что у нас есть два разных типа, Book и Count, которые делают разные вещи. Но их объединяет то, что они оба удовлетворяют интерфейсу fmt.Stringer.
Вы можете думать об этом и наоборот. Если вы знаете, что объект удовлетворяет интерфейсу fmt.Stringer, вы можете положиться на то, что у него есть метод с точной сигнатурой String() string, который вы можете вызвать.
Теперь о важной части.
Везде, где вы видите в Go объявление (например, переменную, параметр функции или поле структуры), имеющее тип интерфейса, вы можете использовать объект любого типа, если он удовлетворяет интерфейсу.
Например, предположим, что у вас есть следующая функция:
Поскольку эта функция WriteLog() использует тип интерфейса fmt.Stringer в объявлении своего параметра, мы можем передать любой объект, который удовлетворяет интерфейсу fmt.Stringer. Например, мы могли бы передать любой из созданных ранее типов Book и Count в метод WriteLog(), и код работал бы нормально.
Кроме того, поскольку передаваемый объект удовлетворяет интерфейсу fmt.Stringer, мы знаем, что он имеет строковой метод String(), который может безопасно вызывать функция WriteLog().
Давайте объединим это в пример, который дает нам представление о силе интерфейсов.
Это довольно круто. Мы создали разные типы Book и Count, но передали их в одну и ту же WriteLog(). В свою очередь, это вызывает их соответствующие функции String() и записывает результат.
Если вы запустите код, вы должны получить вывод, который выгладит следующим образом:
Я не хочу долго останавливаться на этом вопросе. Но ключевой момент, который следует запомнить, заключается в том, что используя тип интерфейса в объявлении нашей функции WriteLog(), мы сделали функцию независимой (или гибкой) от точного типа объекта, который она получает. Важно только то, какие у него есть методы.
Чем они полезны?
Существует множество причин, по которым вы можете в конечном итоге использовать интерфейс в Go, но, по моему опыту, наиболее распространенными являются три:
- Чтобы помочь уменьшить дублирование шаблонного кода
- Упростить использование mock-объектов в unit тестах
- В качестве архитектурного инструмента, помогающего обеспечить разделение между частями вашей кодовой базы.
Давайте пройдемся по этим трем вариантам использования и рассмотрим их более подробно.
Уменьшение шаблонного кода
Хорошо, представьте, что у нас есть структура Customer, содержащая некоторые данные о клиенте. В одной части нашей кодовой базы мы хотим записать информацию о клиенте в bytes.Buffer, а в другой части нашей кодовой базы мы хотим записать информацию о клиенте в os.File на диске. Но в обоих случаях мы хотим с начала сериализовать структуру клиента в JSON.
Это сценарий, в котором мы можем использовать интерфейсы Go, чтобы уменьшить количество шаблонного кода.
Первое, что вам нужно знать, это то, что Go имеет тип интерфейса io.Writer, который выглядит следующим образом:
И мы можем использовать тот факт, что bytes.Buffer, и тип os.File удовлетворяют этому интерфейсу, потому что у них есть методы bytes.Buffer.Write() и os.File.Write() соответственно.
Давайте взглянем на простую реализацию:
Конечно, это всего лишь игрушечный пример ( и есть другие способы структурировать код для достижения того же конечного результата). Но это прекрасно иллюстрирует преимущества использования интерфейса - мы можем создать метод Customer.WriteJSON() один раз, и мы можем вызывать этот метод в любое время, когда мы захотим записать что-то, удовлетворяющее интерфейсу io.Write.
Но если вы новичок в Go, все ровно возникает пара вопросов: откуда вы знаете, что интерфейс io.Writer вообще существует? И как вы заранее знаете, что bytes.Buffer и os.File оба удовлетворяют этому?
Боюсь, здесь нет простого пути - вам просто нужно накопить опыт и познакомится с интерфейсами и различными типами в стандартной библиотеке. Здесь поможет тщательное чтение документации стандартной библиотеки и просмотр чужого кода. Но в качестве быстрого старта я включил список некоторых наиболее полезных типов интерфейсов в конце этого поста.
Но даже если вы не используете интерфейсы из стандартной библиотеки, никто не мешает вам создавать и использовать свои типы интерфейсов. Далее мы расскажем, как это сделать.
Модульное тестирование и mocking
Чтобы проиллюстрировать, как можно использовать интерфейсы для помощи в модульном тестировании, давайте рассмотрим немного более сложный пример.
Допустим вы управляете магазином и храните информацию о количестве покупателей и продаж в базе данных PostgreSQL. Вы хотите написать код, вычисляющий уровень продаж (т.е. объем продаж на одного клиента) за последние 24 часа с округлением до 2 знаков после запятой.
Минимальная реализация кода для этого может выглядеть примерно так:
А, что, если мы хотим создать модульный тест для функции calculateSalesRate(), чтобы убедится, что математическая логика в ней работает правильно?
Так что мы можем сделать? Как вы догадались - интерфейсы спешат на помощь!
Решением здесь является создание нашего собственного типа интерфейса, описывающего методы CountSales() и CountCustomers(), на которые опирается функция calculateSalesRate(). Затем мы можем обновить сигнатуру calculateSalesRate(), чтобы использовать этот настраиваемый тип интерфейса в качестве параметра вместо конкретного типа *ShopDB.
Вот так:
После этого на не составит труда создать макет, удовлетворяющий нашему интерфейсу ShopeModel. Затем мы можем использовать этот макет во время модульных тестов, чтобы проверить, правильно ли работает математическая логика в нашей функции calculateSalesRate(). Вот так:
Вы можете запустить этот тест сейчас, все должно работать нормально.
Архитектура приложения
В предыдущих примерах мы видели, как можно использовать интерфейсы, чтобы отделить определенные части вашего кода от зависимости от конкретных типов. Например функция calculateSalesRate() полностью гибка в отношении того, что вы ей передаете - единственное, что имеет значение, это то, что она удовлетворяет интерфейсу ShopModel.
Вы можете расширить эту идею, чтобы создавать несвязанные "слои" в более крупных проектах.
Допустим вы создаете веб-приложение, которое взаимодействует с базой данных. Если вы создаете интерфейс, описывающий точные методы взаимодействия с базой данных, вы можете ссылаться на интерфейс во всех обработчиках HTTP вместо конкретного типа. Поскольку обработчик HTTP относится только к интерфейсу, и это помогает разделить уровень HTTP и уровень взаимодействия с базой данных. Это упрощает независимую работу со слоями одного слоя в будущем, не затрагивая другой.
Что такое пустой интерфейс?
Если вы какое-то время программировали на Go, вы, вероятно, сталкивались с пустым типом интерфейса: interface{}. Это может сбивать не много с толку, но я попытаюсь объяснить.
В начале этого поста я сказал:
Тип интерфейса в Go похож на определение. Он определяет и описывает точные методы, которые должен иметь какой-либо другой метод.
Пустой тип интерфейса по существу не описывает никаких методов. В нем нет правил. И из этого следует, что любой объект удовлетворяет пустому интерфейсу.
Или, выражаясь более простым языком, пустой тип интерфейса interface{} похож на подстановочный знак. Везде, где вы видите это в объявлении (например, переменная, параметр функции или поле структуры), вы можете использовать объект любого типа.
Взгляните на следующий код:
В этом фрагменте кода мы инициализируем person map, которая использует строковой тип для ключей и пустой тип интерфейса interface{} для значений. Мы назначили три разных типа в качестве значений map (string, int и float32) - и это нормально. Поскольку объекты любого типа удовлетворяют пустому интерфейсу, код будет работать нормально.
Вы можете попробовать запустить код здесь и при запуске вы должны увидеть результат, который выглядит вот так:
Но есть важная вещь, на которую следует обратить внимание, когда дело доходит до извлечения и использования значения из этой mapы.
Например, предположим, что мы хотим получить значение "возраст" и увеличить его на 1. Если вы напишите что-то вроде следующего кода, он не скомпилируется:
И вы получите следующее сообщение об ошибке:
Это происходит потому, что, значение, хранящееся в mape, имеет тип interface{}, а не int, поэтому мы не можем к нему прибавить 1.
Чтобы обойти это необходимо сначала привести к типы int перед прибавлением 1. Вот так:
Если вы сейчас запустите это, все будет работать:
Итак, когда вы должны использовать пустой тип интерфейса в своем собственном коде?
Ответ, вероятно, не так часто. Если вы обнаружите, что тянетесь к нему, сделайте паузу и подумайте, действительно ли использование interface{} является правильным вариантом. Как правило, понятнее, безопаснее и эффективнее использовать конкретные типы или непустые интерфейсные типы. В приведенном выше фрагменте кода было бы более уместно определить структуру Person с соответствующими типизированными полями, подобными этому:
Но при этом пустой интерфейс полезен в ситуациях, когда вам нужно принимать и работать с непредсказуемыми или определяемыми пользователями типами. Именно по этой причине вы увидите, что он используется во многих местах стандартной библиотеки, например в функциях gob.Encode, fmt.Print и template.Execute.
Распространенные и полезные типы
Наконец, вот краткий список некоторых из наиболее распространенных и полезных интерфейсов в стандартной библиотеке. Если вы с ними еще не знакомы, я рекомендую потратить немного времени на просмотр соответствующей документации по ним:
Кроме того, в этом списке находится более полный список стандартных библеотек.
Источник: https://www.alexedwards.net/blog/interfaces-explained
#golang #перевод #программирование