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 мая. Регистрация доступна по ссылке ниже.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/667308/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *