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