Источник: Nuances of Programming
Часть 1, Часть 2
В предыдущей статье мы говорили о создании «идеальной» настройки для Golang проекта. Теперь пришла пора найти для неё реальное применение: будем создавать интерфейсы RESTful API. В этой части рассмотрим базы данных, модульное тестирование, тестирование API, пример приложения и в принципе всё, что вам нужно для создания реального проекта. Рекомендую ещё раз перечитать предыдущую статью. И вот теперь приступим, не теряя времени. Поехали!
Краткое изложение доступно в моём репозитории (в ветке rest-api) — https://github.com/MartinHeinz/go-project-blueprint/tree/rest-api
Платформы и библиотеки
Во-первых, что мы будем использовать?
- Gin — каркас для разработки веб-приложений с применением HTTP-протокола. Это высокопроизводительная платформа на net/http с самыми необходимыми программными средствами, библиотеками и функциональными возможностями. К тому же у неё довольно аккуратный и развитый интерфейс.
- GORM — библиотека средств объектно-реляционного отображения Golang, разработанная на database/sql. В неё включены такие функции, как предзагрузка, обратные вызовы, транзакции и другие. Здесь придётся потратить немного времени на освоение, и документация не так крута. Но если вы из тех людей, что предпочитают писать запросы на голом SQL, то вполне можете довольствоваться sqlx.
- Viper — библиотека конфигураций Go, которая работает с разными форматами, параметрами командной строки, переменными среды и т.д. Всех интересующихся настройкой и использованием этой библиотеки направляем в предыдущую статью, где всё это подробно расписано.
Проект и структура пакетов
Перейдём теперь к отдельным пакетам проекта. Сначала рассмотрим пакеты, связанные с базами данных, потом — с запросами, и доберёмся до конечных точек API. Кроме пакета main, есть пакеты, каждый из которых следует принципу единственной ответственности:
Модели
Пакет моделей (models) имеет один файл, который определяет типы, отражающие структуру таблицы базы данных. В примере из репозитория есть 2 типа struct — Model и User:
Model — это тот же тип, что и gorm.Model, только с тегами json: так проще генерировать ответы JSON, содержащие его поля. User описывает простого пользователя приложения с тегами GORM, указывающими на столбец, с которым связано поле. Есть также теги для индексов, типов, ассоциаций и т.д. Узнать о них больше можно здесь.
Объекты доступа к данным
Дальше идёт пакет daos, расшифровывается как Data Access Objects (DAOs). DAO — это объект, отвечающий за доступ к данным. Он выполняет SQL-запросы, используя GORM или голый SQL. Например, у нас есть простая функция, которая получает данные о пользователе с помощью ID и возвращает их в виде модели User вместе с ошибкой, если ошибка имеется:
Можно разделить объекты доступа к данным по какому-либо критерию, например по таблицам, к которым данные имеют доступ, либо по какой-то другой логике. Только не сваливайте всё в одну кучу, иначе будет неразбериха.
Сервисы
Отлично. Данные у нас аккуратно загружены в модели. Прежде чем их отдавать, можно использовать дополнительную логику для обработки данных. И здесь в дело вступают сервисы. Такой дополнительной логикой может быть фильтрация, агрегирование, изменение структуры или валидация данных. К тому же это позволяет отделять запросы к базе данных от логики предметной области, делая код намного более чистым, простым для сопровождения и — что лично для меня самое важное — легко тестируемым (мы ещё поговорим об этом дальше). Посмотрим на код:
Здесь мы сначала определяем интерфейс, который объединяет все ранее созданные функции DAO, в нашем случае просто Get(id uint) из предыдущего кода. Потом определяем сервис User с нашим объектом доступа к данным и функцию, которая его создаёт, используя DAO в качестве параметра. Наконец, определяем функцию, которая может задействовать дополнительную логику и использовать DAO из UserService. Здесь для простоты используем DAO при выполнении запроса в базу данных на пользователя и возвращаем DAO. Примером используемой здесь логики может быть валидация модели или проверка на наличие ошибок.
API-интерфейсы
И, наконец, используя эти сервисы, дающие нам обработанные и валидные данные, мы можем предоставлять их нашим пользователям. Обратимся к коду:
Здесь у нас функция, которую можно использовать для работы конечной точки API-интерфейса. Сначала создаём сервис с заданным пользователем DAO. Затем парсим ID, который мы ожидаем в URL (что-то вроде /users/{id}), потом используем сервис для получения из БД данных о пользователе. Если данные будут найдены, возвращаем их в формате JSON с кодом состояния 200.
Объединяем всё вместе
Выглядит здорово, но сейчас нам надо всё это настроить в main, чтобы Gin понимал, где работают наши API:
Сначала нужно создать экземпляр Gin, потом привяжем к нему промежуточное ПО (logger или CORS). И самое важное — создаем набор конечных точек (все они будут начинаться с api/v1/) и регистрируем нашу функцию GetUser, чтобы она работала в /api/v1/users конкретного пользователя (определяемого параметром ID). Вот и всё, теперь можно запускать наше приложение!
Возможно, вы подумали: «Зачем создавать все эти пакеты, отдельные файлы, функции в несколько слоёв и т.д. и т.п.?». Но если в вашем приложении всё будет свалено в кучу, то со временем, когда приложение станет достаточно большим, проблем с сопровождением кода будет не избежать. А самое важное, на мой взгляд: такое разделение необходимо для лучшей тестируемости, ведь гораздо легче тестировать каждый уровень — доступ к базе данных, управление данными и API — отдельно, чем всё в одном месте. Раз уж речь зашла о тестах, неплохо было бы попрактиковаться в их написании…
Обратите внимание: из фрагмента кода, приведённого выше, я убрал несколько строчек и комментариев (например, подключение к базе данных или загрузка конфигурации). Сделано это было для простоты и ясности. Все убранные строчки вместе с дополнительными пояснениями и комментариями можно найти в репозитории.
Настройка для тестов
Вот и добрались до тестов, моей любимой части! Начнём с пакета test_data. Здесь содержатся служебные функции, связанные с тестовой базой данных и тестовыми данными. Хотел бы обратить ваше внимание на функцию init:
Эта функция особенная: Go выполняет её, когда пакет импортируется. Здесь можно выполнить настройку для тестов: сначала загружаем конфигурацию, потом создаём тестовую базу данных (SQLite в оперативной памяти), для которой мы активируем внешние ключи. Затем создаём таблицы базы данных, используя функцию GORM AutoMigrate.
А здесь вы могли подумать: «Зачем использовать базу данных SQLite в оперативной памяти? Неужели так лучше?». Вообще-то да. Сам я использую для всех проектов PostgreSQL. Когда же дело доходит до тестов, нужно что-то понятное и предсказуемое, быстрое (в оперативной памяти) и независимое от хост-системы/сервера баз данных — всё это обеспечивает данная настройка.
Не будем переходить к оставшимся функциям пакета, а то вам надоест читать, к тому же они уже есть у нас здесь.
Помимо функции инициализации, в пакете у нас хранятся кое-какие данные. Например, файл db.sql, в котором содержатся: а) инструкции вставок SQL, добавляющие значения в базу данных SQLite перед запуском тестов; б) тестовые случаи в формате JSON, используемые как ожидаемые результаты для конечных точек API.
Теперь, когда наша тестовая настройка готова, перейдём к тестам в каждом пакете:
Это тест объектов доступа к данным daos, он очень простой: создаём DAO, вызываем тестируемую функцию (Get) и проверяем на соответствие ожидаемым значениям, добавленным в базу данных SQLite во время настройки. Больше тут добавить нечего — переходим к services:
Здесь кода побольше, пробежимся по нему снизу вверх. Первое, что нам нужно, — сымитировать DAO (mockUserDAO), дабы не зависеть от реализации настоящего DAO. Чтобы эта имитация имела смысл, нужно заполнить её тестовыми данными, что и происходит в newMockUserDAO. Дальше определяем версию Get, которая имитирует настоящую: вместо выполнения запроса к базе данных, мы просматриваем ненастоящие записи и возвращаем, если найдём заданный ID.
Для самого теста создаём NewUserService, но вместо настоящего DAO, используем нашу имитацию с предсказуемым поведением, так что мы можем изолировать проверяемую в тесте функцию от основного DAO. Осталось только выполнить простой тест: используем сымитированный метод Get и проверяем на наличие ожидаемых значений, добавленных нами в имитацию.
Последним проверяем API, все тесты здесь длиной практически в одну строку, но немного подготовиться нам всё же придётся:
Для целей нашего тестирования здесь три функции имитируют HTTP-запрос. Первая функция создаёт Gin в тестовом режиме и возвращает базу данных в исходное состояние. Вторая наблюдает за URL и затем отправляет запрос в конкретную конечную точку API. Третья функция запускает список тестовых случаев и проверяет, не совпадают ли коды состояния, и дополнительно может проверить, не совпадают ли результаты в формате JSON. Рассмотрим примеры тестовых случаев:
Параметров целая куча, но здесь всё довольно просто. Разберём каждый параметр:
- "t1 - get a User" — название тестового случая с номером для облегчения поиска при отладке;
- "GET" — метод HTTP;
- "/users/:id" — тестируемый URL;
- "/users/1" — конкретный URL с включёнными параметрами;
- "" — тело запроса, в данном случае пустое;
- GetUser — метод, прикреплённый к конечной точке;
- http.StatusOK — ожидаемый код состояния, здесь 200;
- path + "/user_t1.json" — путь к ожидаемому результату в формате JSON, все хранятся в пакете test_data.
Заключение
Вот и всё, что нужно для создания RESTful API в Golang. Надеюсь, хоть что-то из изложенного здесь будет кстати при создании вашего следующего проекта. Весь исходный код находится здесь. Если статья понравилась, переходите к следующей, в которой мы расскажем, как добавить крутую документацию к вашему проекту.
Читайте также:
Читайте нас в телеграмме и vk
Перевод статьи Martin Heinz: Building RESTful APIs in Golang (впервые опубликована на martinheinz.dev).