Go в API для мобильного приложения. Создаем совместный список покупок с мгновенными уведомлениями
В предыдущей статье мы рассмотрели использование Go для создания веб-приложений (с выполнением через Web Assembly). Но прежде всего Go интересен как язык для реализации высокопроизводительных и неблокирующих решений на стороне сервера и в этой статье мы изучим использование Go для backend на примере разработки API для мобильного приложения для совместного редактирования списка покупок. Приложение будет включать в себя механизмы авторизации, запроса и модификации объектов, а также мгновенные уведомления (через веб-сокеты и Push) и мониторинг доступность API. В качестве примера мы создадим минимальный API, для которого обеспечивается уведомление всех зарегистрированных пользователей об изменении списка, а также будут предусмотрена отправка пуш-уведомлений всем адресатам по запросу.
Существует огромное количество библиотек для Go, включающие все возможные сценарии использования, но нам нужно будет подобрать подходящий стек для решения следующих задач:
взаимодействия с базой данных (идеально было бы сделать с поддержкой объектно-реляционного отображения — ORM);
реализация REST-API для управления ресурсами системы;
авторизация пользователя и выдача токена доступа к API (JWT);
поддержка постоянного подключения с клиентскими приложениями через веб-сокеты;
отправка пуш-уведомлений зарегистрированным клиентам;
генерация документации по использования API;
мониторинг доступности сервиса.
Взаимодействие с базой данных
Go поддерживает низкоуровневые драйверы для большинства свободных и проприетарных баз данных (включая Oracle, MSSQL и MySQL, PostgreSQL, MongoDB, …) и позволяет выполнять и генерировать SQL-запросы с использованием определения схемы данных в тэгах Go. Одной из наиболее активно развивающихся и функциональных библиотек для объектно-реляционного отображения можно считать GORM, позволяющий описывать схему данных внутри структур, определять отношения между структурами, автоматически создавать миграции при изменении схемы данных, создавать запросы (в том числе, с поддержкой JOIN) и управлять транзакциями.
Начнем прежде всего с определения схемы данных для хранения списка покупок. Мы ограничимся в нашем приложении (для простоты реализации) одним списком для всех пользователей, но нет никаких сложностей сделать раздельные списки и проверку уровней доступа для зарегистрированных пользователей.
Наши объекты будут состоять из следующих полей:
Название (строка).
Отметка о выполнении (логическое значение).
Автор (ссылка на пользователя).
Исполнитель, завершивший задачу (ссылка на пользователя), может быть null.
Дата-время добавления (timestamp).
Дата-время выполнения (timestamp), может быть null.
Кроме списка продуктов будет необходимо создать таблицу с пользователями (мы будем сохранять e-mail для уникальной идентификации).
Первичный ключ (число, автоинкремент).
Адрес электронной почты (строка).
Хэш пароля (строка).
Дата-время последнего входа в систему (timestamp).
Начнем разработку приложения с установки библиотеки gorm и создания структур, описывающих модель данных:
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
Для корректной установки на компьютере должен быть установлен gcc (например, на Debian/Ubuntu он может быть добавлен через apt install build-essential, на Windows через Msys2 и pacman -Syu && pacman -S --needed base-devel mingw-w64-x86_64-toolchain).
Добавим в проект определение структур (в пакет models):
package models
type ShoppingListItem struct {
gorm.Model
Name string
Finished bool
Creator User `gorm:"foreignKey:Id"`
Performer *User `gorm:"foreignKey:Id"`
Added int64
Completed *int64
}
type User struct {
gorm.Model
Id int32 `gorm:"PrimaryKey"`
EMail string
PasswordHash string
LastUpdate int64
}
И выполним инициализацию базы данных в main:
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"shoppinglist/models"
)
func main() {
db, err := gorm.Open(sqlite.Open("shoppinglist.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// Migrate the schema
db.AutoMigrate(&models.User{}, &models.ShoppingListItem{})
}
После запуска приложения будет создан файл данных для SQLite, содержащий две таблицы в соответствии с описанием структур Go. Заполним несколькими тестовыми записями:
func populateTestData(db *gorm.DB) {
var newUser = models.User{
EMail: "test.shopping.list@gmail.com",
PasswordHash: "098f6bcd4621d373cade4e832627b4f6",
LastUpdate: time.Now().Unix(),
}
db.Create(&newUser)
db.Create(&models.ShoppingListItem{
Name: "Milk",
Finished: false,
Creator: newUser,
Performer: nil,
Added: time.Now().Unix(),
Completed: nil,
})
db.Create(&models.ShoppingListItem{
Name: "Coffee",
Finished: false,
Creator: newUser,
Performer: nil,
Added: time.Now().Unix(),
Completed: nil,
})
}
Для упрощения отладки изменим конфигурацию логирования:
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
db, err := gorm.Open(sqlite.Open("shoppinglist.db"), &gorm.Config{Logger: newLogger})
И добавим после заполнения данных диагностику по количеству записей в базах данных. Обратите внимание, что название базы данных (как и полей) отличается от исходных названий структур — camelcase заменяется на snakecase, в конце добавляется символ «s» (для множественного числа):
var usersCount int64
db.Table("users").Count(&usersCount)
println("Users count is ", usersCount)
var shoppingListItemCount int64
db.Table("shopping_list_items").Count(&shoppingListItemCount)
println("Shopping list items count is ", shoppingListItemCount)
Для выполнения запросов извлечения и модификации данных нам будут доступны методы:
db.Delete(объект)
илиdb.Delete(&ShoppingListItem{}, 1)
для удаления (соответственно для ранее полученного объекта или по типу данных и первичному ключу).db.Save(объект)
для обновления ранее полученного объекта.db.First(&obj, 1)
для извлечения объекта по ключу (таблица определяется по типу переменной obj).db.Find(&obj, []int{1,2,3})
для извлечения группы объектов с заданными первичными ключами.db.Find(&objs)
для получения всех объектов из таблицы (определяется по типу элемента списка objs).db.Table("shopping_list_item").Select("name").Rows()
для извлечения названий всех продуктов из списка покупок. Дополнительно можно задать условия поиска (.Where), упорядочивание набора (.Order), ограничение количества (.Limit).
Эти методы мы будем использовать в контроллере для REST API. Например, для получения продуктов из списка покупок с упорядочиванием по времени в обратном хронологическом порядке можно использовать следующий код:
rows, err := db.Table("shopping_list_items").Select("name").Order("created_at desc").Rows()
for rows.Next() {
var name string
rows.Scan(&name)
println(name)
}
Мы создали заготовку кода для взаимодействия с базой данных и теперь можем перейти к созданию REST API. Выбор сетевых библиотек для Go чрезвычайно велик (начиная от встроенного net/http, высокопроизводительного fasthttp, маршрутизаторами gorilla/mux и до сложных фреймворков вроде gin-gonic или Echo), поэтому здесь сложнее выбрать предпочтительное решение, поэтому будем использовать один из наиболее известных и достаточно удобных в применении фреймворк gin-gonic. Выбор также определяется возможностями встраивания Middleware для обработки запроса (например, проверки аутентификации) и наличии инструментов генерации документации на основе зарегистрированных обработчиков адресов.
REST API-сервис на Gin
Начнем с установки модуля: go get -u github.com/gin-gonic/gin.
Добавим новую функцию для инициализации сервера и настройки маршрутов.
func initAPI() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "OK"})
})
r.Run()
}
В список импортов должно быть добавлено "github.com/gin-gonic/gin"
(сам фреймворк) и "net/http"
для поддержки констант состояния ответа (например, http.StatusOK).
Для маршрута указывается функция, принимающая контекст запроса (gin.Context), который используется для получения информации о запросе (извлечение значения именованных параметров, десериализация JSON в структуру Go), а также для формирования ответа (может быть отправлен как плоский текст, сериализация структуры Go в Json, либо двоичный файл). Для описания JSON-полей можно использовать встроенные договоренности по описанию схемы JSON-сериализации (и их можно добавить непосредственно в поля структуры, которая описывает модель данных для объединения DAO и DTO). Создадим контроллер для управления списком покупок (пока без авторизации):
Прежде всего добавим описание json-полей в структуру ShoppingListItem:
type ShoppingListItem struct {
gorm.Model
Name string `json:"name"`
Finished bool `json:"finished"`
Creator User `gorm:"foreignKey:Id" json:"creator"`
Performer *User `gorm:"foreignKey:Id" json:"performer"`
Added int64 `json:"added"`
Completed *int64 `json:"completed"`
}
Создадим теперь контроллер для реализации REST-методов со списком покупок и отдельный пакет для работы с базой данных:
package data
import (
"gorm.io/gorm"
"shoppinglist/models"
)
type Data struct {
Db *gorm.DB
}
func (data *Data) GetItems() []models.ShoppingListItem {
var items = make([]models.ShoppingListItem, 0, 0)
data.Db.Find(&items)
return items
}
func (data *Data) GetItem(id int) *models.ShoppingListItem {
var result *models.ShoppingListItem
data.Db.Find(&result, id)
return result
}
func (data *Data) DeleteItem(id int) bool {
err := data.Db.Delete(&models.ShoppingListItem{}, id)
return err == nil
}
func (data *Data) CreateItem(item models.ShoppingListItem) uint {
data.Db.Create(&item)
return item.ID
}
func (data *Data) UpdateItem(item models.ShoppingListItem) {
data.Db.Save(item)
}
Контроллер будет выполнять проксирование HTTP-запросов в вызовы методов для взаимодействия с базой данных:
type Controller struct {
Data data.Data
}
func (controller *Controller) GetItems(c *gin.Context) {
c.JSON(http.StatusOK, controller.Data.GetItems())
}
func (controller Controller) GetItem(c *gin.Context) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, controller.Data.GetItem(id))
}
func (controller Controller) DeleteItem(c *gin.Context) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if controller.Data.DeleteItem(id) {
c.JSON(http.StatusOK, gin.H{})
} else {
c.JSON(http.StatusNotFound, gin.H{})
}
}
func (controller Controller) CreateItem(c *gin.Context) {
var item models.ShoppingListItem
err := c.ShouldBindJSON(&item)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
} else {
id := controller.Data.CreateItem(item)
c.JSON(http.StatusOK, id)
}
}
func (controller Controller) UpdateItem(c *gin.Context) {
var item models.ShoppingListItem
err := c.ShouldBindJSON(&item)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
} else {
controller.Data.UpdateItem(item)
c.JSON(http.StatusOK, gin.H{"status": "updated"})
}
}
И добавим соответствующие маршруты в инициализацию Gin:
r.GET("/item", controller.GetItems)
r.GET("/item/:id", controller.GetItem)
r.UPDATE("/item/:id", controller.UpdateItem)
r.DELETE("/item/:id", controller.DeleteItem)
r.POST("/item", controller.CreateItem)
Здесь мы используем возможности разбора JSON-тела запроса (ShouldBindJSON), который возвращает ошибку при несоответствии схемы данных. Также через контекст могут быть получены фрагменты пути : name, параметры GET-запроса, присоединенные файлы и информация об агенте (браузере или приложении). Для формирования ответа используется метод контекста JSON, который определяет код ответа (используется библиотека net/http), а также его содержание в виде сериализуемой map (через gin.H) или структуру с описанием json-полей (в этом случае она будет автоматически преобразована в JSON-ответ).
Следующим этапом мы добавим авторизацию на выполнение запросов, а для этого нужно будет подключить Middleware с поддержкой токена запроса.
Контроль доступа к ресурсам
Существует два метода для проверки прав доступа и отслеживания пользователя. Первый из них — создание сессии (чаще используется при создании серверной стороны веб-приложений, где пользователь получает сгенерированные страницы), для этого можно использовать Middleware из github.com/gin-gonic/contrib/sessions:
var secret = []byte("secret")
r.Use(sessions.Sessions("mysession", sessions.NewCookieStore(secret)))
Вызов Use регистрирует middleware для управления сессиями и далее оно отслеживает текущего пользователя (в этом случае — с использование cookie, но могут быть и иные варианты). Middleware представляет из себя функцию, с заголовком идентичным обработчику запроса (принимает *gin.Context), которая может выполнить изменение запроса или создать ответ до вызова основного обработчика из маршрутизатора. Например, для ограничения доступности некоторых маршрутов можно обернуть часть вызовов в Use с контролем авторизации пользователя через сессию (для этого удобно использовать возможность создания групп запросов с общим префиксом) и реализовать методы для авторизации и выхода из системы, которые будут управлять значением в хранилище сессии с ключом username.
private := r.Group("/user")
private.Use(AuthRequired)
{
private.GET("/me", me)
private.GET("/status", status)
}
func AuthRequired(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("username")
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
func Login(c *gin.Context) {
session := sessions.Default(c)
username := c.PostForm("username")
password := c.PostForm("password")
if username=="user" && password=="password" {
session.Set("username", "user")
session.Save()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
}
}
func Logout(c *gin.Context {
session := sessions.Default(c)
session.Delete("username")
}
Однако для REST API предполагается, что каждый запрос является полностью независимым и не зависит от предыдущего состояния сервера. В большинстве случаев для авторизации сетевых запросов используют постоянные или временные токены, которые отправляются в заголовке Authorization. Де-факто во многих сервисах используется механизм выдачи и обновления токенов JWT, так что добавим себе в приложение поддержку этого способа определения источника запроса.
JWT-токены выдаются со стороны сервера после выполнения аутентификации любым другим способом (например, по парольной паре) и предполагают периодическое обновление для исключения потенциальной утечки. Для корректного функционирования JWT публикует три точки подключения — авторизация, обновление токена и инвалидация (удаление токена). Для интеграции с Gin Framework существует несколько библиотек для поддержки JWT, мы рассмотрим добавление авторизации на примере Gin-JWT.
Выполним установку:
go get github.com/appleboy/gin-jwt/v2
И добавим необходимый импорт"github.com/appleboy/gin-jwt/v2"
.
Далее необходимо сконфигурировать middleware, это включает в себя как описание временных параметров ключей (время жизни, периодичность обновления), так и включение кода для проверки корректности учетных данных пользователя при авторизации и реакцию на некорректный доступ.
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
type User struct {
UserName string
FirstName string
LastName string
}
func attachLogin(r *gin.Engine) {
authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
Realm: "Shopping List API",
Key: []byte("myverysecretkey"),
Timeout: time.Hour, //время до истечения ключа
MaxRefresh: time.Hour, //требуемая переодичность обновления
IdentityKey: identityKey, //атрибут для хранения идентификатора
PayloadFunc: func(data interface{}) jwt.MapClaims { //создание информации о пользователе
if v, ok := data.(*User); ok {
return jwt.MapClaims{
identityKey: v.UserName,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} { //создание объекта с описанием пользователя
claims := jwt.ExtractClaims(c)
return &User{
UserName: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) { //выполнение авторизации и выдача ключа
var loginVals login
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
password := loginVals.Password
if userID == "admin" && password == "admin" {
return &User{
UserName: userID,
LastName: "Admin",
FirstName: "Admin",
}, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool { //функция авторизации
if v, ok := data.(*User); ok && v.UserName == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) { //ответ при неавторизованном доступе
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
TokenLookup: "header: Authorization, query: token, cookie: jwt", //где будем искать токен авторизации
TokenHeadName: "Bearer", //тип токена (по умолчанию Bearer)
TimeFunc: time.Now, //функция для генерации времени
})
if err != nil {
log.Fatal("JWT Error:" + err.Error())
}
errInit := authMiddleware.MiddlewareInit()
if errInit != nil {
log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
}
r.POST("/login", authMiddleware.LoginHandler)
r.GET("/logout", authMiddleware.LogoutHandler)
auth := r.Group("/auth")
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/me", meHandler)
}
}
Добавим вызов attachLogin в инициализацию Gin, после чего получим возможность выполнять вход в систему (POST-запрос на адрес /login с полями формы username и password), в ответ будет возвращен токены доступа/обновление и срок действия, выполнить обновление токена (GET запрос /refresh_token с передачей текущего токена обновления), а также выполнять запросы в защищенной области (в этом коде это /auth/me, но аналогично можно обернуть все запросы к API для управления списком покупок с использованием authMiddleware.MiddlewareFunc). Для доступа к информации об авторизованном пользователе будет использоваться контекст:
func meHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c)
user, _ := c.Get(identityKey)
c.JSON(200, gin.H{
"userID": claims[identityKey],
"userName": user.(*User).UserName,
})
}
Разумеется, в настоящем приложении нам нужно будет добавить методы проверки пользователей по базе данных (создаются аналогично рассмотренным ранее методам работы со списком покупок), а также сценарии для регистрации нового пользователя и сброса пароля, но оставим эти вопросы на вашу самостоятельную проработку.
Таким образом мы добавили контроль доступа к ресурсам и механизмы генерации и обновления токенов для авторизованных пользователей. Следующим шагом мы добавим поддержку веб-сокетов для отправки уведомлений клиентам об изменении списка задач.
Поддержка веб-сокетов для отправки уведомлений
При создании мобильных приложений с совместным доступом к данным очень важно обеспечить мгновенную отправку уведомлений об изменениях. Существует два альтернативных подхода — с использованием веб-сокетов (постоянно существующих подключений, требует запущенного приложения в фоновом или полноэкранном режиме), либо с применением push-уведомлений платформы (в этом случае они могут быть получены и обработаны без необходимости запуска приложения). Рассмотрим первый вариант реализации.
Для включения поддержки веб-сокетов в gin можно использовать библиотеку Melody, предоставляющая возможность обработки запросов через web socket непосредственно внутри обработчиков подключений в gin.
Начнем с установки библиотеки:
go get gopkg.in/olahol/melody.v1
и добавим импорт "gopkg.in/olahol/melody.v1".
Затем, вместе с gin мы должны будем инициализировать Melody и привязать ее обработчики к одному из адресов в маршрутизаторе gin. Для веб-сокетов реализуется несколько обработчиков — при открытии сессии (HandleConnect), получении сообщения (HandleMessage) и отключении клиента (HandleDisconnect). Например, в нашем случае при подключении нового клиента мы добавим ссылку на его сессию в список получателей, при отключении будем исключать его из списка. Обработка входящих сообщений не является в этом приложении обязательной, поскольку запросы модификации списка поступают через REST.
sessions := make(map[]melody.Session)
m := melody.New()
m.HandleConnect(func(session *melody.Session) {
println("Connected")
id, _ := uuid.NewV4()
//добавить новую сессию
session.Set("uuid", id.String())
sessions = append(sessions, session)
})
m.HandleDisconnect(func(session *melody.Session) {
println("Disconnected")
//удалить завершенную сессию
my_uuid, _ := session.Get("uuid")
for i := 0; i < len(sessions); i++ {
uuid, _ := sessions[i].Get("uuid")
if my_uuid.(string) == uuid.(string) {
sessions = append(sessions[:i], sessions[i+1:]...)
break
}
}
})
m.HandleMessage(func(session *melody.Session, msg []byte) {
var message models.Message
json.Unmarshal(msg, &message)
}
r.GET("/ws", func(c *gin.Context) {
m.HandleRequest(c.Writer, c.Request)
})
Для подключения по протоколу Web Socket в этом примере будет использоваться путь /ws. При создании подключения генерируется уникальный идентификатор сессии, который в дальнейшем может использоваться для определения связанного пользователя (в сочетании с токеном авторизации). При отключении клиента зарегистрированная в списке сессия удаляется. Список сессий можно использовать, например, для отправки уведомления всем получателям (при добавлении нового элемента в списке покупок или отметке о выполнении на одном из существующих).
var message models.Message
var msg []byte
json.Marshal(msg, &msg)
for _, session := range sessions {
session.Broadcast(msg)
}
Использование пуш-уведомлений
В отличии от веб-сокетов платформенные пуш-уведомления не определяются едиными стандартами и требуют поддержки со стороны библиотеки варианта реализации конкретного поставщика. Наиболее популярными сервисами для отправки пуш-уведомлений являются Firebase Cloud Messaging (Google FCM) и Apple Push Notification Services (APN), а также вендорные решения Huawei Notification. Наиболее универсальным решением для отправки пуш-уведомлений сейчас можно назвать сервер рассылки уведомлений gorush, который может взаимодействовать с Google FCM, APN, Huawei Messaging Service и запускается как отдельный сервис (из контейнера appleboy/gorush) и может работать как через утилиту командной строки gorush, так и программно через протокол gRPC. Также можно использовать платформенные библиотеки go-gcm и apns2.
Прежде всего для настройки рассылки пуш-уведомлений необходимо получить токены авторизации в соответствующих сервисах для разработки. Далее ключи (или сертификаты) передаются в yaml-конфигурации контейнера (подробнее в официальной документации). Для доступа к запущенному сервису можно использовать gRPC подключение и описание протокола gorush.
import (
"github.com/appleboy/gorush/rpc/proto"
structpb "github.com/golang/protobuf/ptypes/struct"
"google.golang.org/grpc"
)
const (
address = "gorush:9000"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure()) //подключение к серверу
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := proto.NewGorushClient(conn)
//отправка уведомления
r, err := c.Send(context.Background(), &proto.NotificationRequest{
Platform: 2,
Tokens: []string{"1"},
Message: "В список покупок добавлены помидоры", //текст сообщения
Badge: 1,
Category: "shopping", //категория сообщения
Sound: "default",
Priority: proto.NotificationRequest_HIGH,
Alert: &proto.Alert{
Title: "Добавлен продукт",
Body: "В список покупок добавлены помидоры",
Subtitle: "1 кг",
},
//дополнительные данные
Data: &structpb.Struct{
Fields: map[string]*structpb.Value{
"shopping_id": {
Kind: &structpb.Value_StringValue{StringValue: "welcome"},
},
"key2": {
Kind: &structpb.Value_NumberValue{NumberValue: 2},
},
},
},
})
if err != nil {
log.Println("could not send notification: ", err)
}
}
Генерация документации по API
Для автоматического создания документации по доступным точкам подключения можно использовать препроцессор исходных текстов gin-swagger, анализирующий блок комментариев перед функцией обработчиком маршрута и создающий описание в виде swagger yaml-файла, который может быть опубликован непосредственно на этом же сервере.
import (
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
Аннотация // @BasePath /api/v1 интерпретируется как определение общего префикса для API. Для корректной интерпретации схемы входных и выходных данных к запросу перед методом контроллера необходимо добавить блок комментариев. Например, для метода получения информации о элементе списка комментарий может выглядеть следующим образом:
// Получение информации о элементе списка покупок
// @Summary Получение информации о элементе списка покупок
// @Description Возвращает подробное описание элемента списка покупок
// @Param id path string true "Идентификатор элемента"
// @Produce json
// @Success 200 {object} []models.ShoppingListItem "Информация о элементе"
// @Failure 400 {object} httputil.HTTPError
// @Router /item/{id} [GET]
func (controller Controller) GetItem(c *gin.Context) {
ids := c.Param("id")
id, err := strconv.Atoi(ids)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.JSON(http.StatusOK, controller.Data.GetItem(id))
Здесь указывается как общее описание метода (Summary / Description), так и схема входных данных (@Param), тип принимаемых данных (@Accept), тип возвращаемого результата (@Produce) и возможные статусы успешного (@Success) и неуспешного (@Failure) выполнения. Для генерации документации в сценарий сборки необходимо включить запуск консольной утилиты препроцессора:
go get -u github.com/swaggo/swag/cmd/swag
$GOPATH/bin/swag init
Для публикации созданного swagger-файла и интерфейса для его просмотра в маршрутизатор Gin необходимо добавить следующие действия:
url := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition
.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
Проверка доступности сервиса
В простом случае проверка корректности функционирования сервиса может быть выполнена как отдельная функция с проверками доступности необходимых подсистем, присоединенная на известный маршрут (например, GET /status). Также существует несколько middleware для автоматической проверки доступности маршрутизатора (например, gin-health-check) и инструментов для проверки доступности внешних систем и создания JSON-ответа по текущему состоянию. Например, можно использовать библиотеку health, предоставляющую возможность регистрировать проверки доступности внешних сервисов.
Установим библиотеку:
go get -u github.com/alexliesenfeld/health
Добавим импорт health «github.com/alexliesenfeld/health» и зарегистрируем проверку доступности SQLite3:
checker := health.NewChecker(
health.WithCacheDuration(1*time.Second),
health.WithTimeout(10*time.Second),
health.WithPeriodicCheck(15*time.Second, 3*time.Second, health.Check{
Name: "database", // название проверки
Timeout: 2 * time.Second, // таймаут проверки
Check: db.PingContext,
}),
//перехват изменения состояния
health.WithStatusListener(func(ctx context.Context, state health.CheckerState) {
log.Println(fmt.Sprintf("health status changed to %s", state.Status))
}),
)
http.Handle("/health", health.NewHandler(checker))
go http.ListenAndServe(":3000", nil)
Проверка доступности будет публиковаться на другой порт (3000), чтобы исключить ситуацию пропуска ошибки из-за недоступности основного маршрутизатора Gin.
Таким образом мы создали полноценный API с автоматической проверкой состояния доступности, генерацией документации, поддержкой REST-методов для взаимодействия с мобильным приложениям, а также возможностью мгновенной передачи уведомлений через постоянно открытые web-сокеты и платформенные пуш уведомления. Также для реальных задач бывает необходимо настроить политики CORS, ограничивать количество запросов (для исключения атак), выполнять отладку исполнения запросов и собирать аналитику по времени их обработку. Для многих из этих задач существует готовые middleware (например, можно посмотреть в подборку gin-contrib), но архитектура решения позволяет без особых затруднения создавать собственную сложную логику обработки запросов.
Полный текст API доступен в Github.
Ну и по традиции всех, кто дочитал до конца, хочу пригласить на бесплатный урок по теме «Функции и методы в Golang». Урок пройдет уже 25 мая. Регистрация доступна по ссылке ниже.