В этой лабораторной работе мы разработаем простой веб-сервер на основе языка Golang и фреймворка gin-gonic.
-
Вводная часть: новый проект
-
Выбираем технологии: почему не подходят шаблоны?
-
API и REST: что это и почему это удобно?
3.1 Что такое API?
3.2 Что такое REST? Как его применить для нашей задачи?
-
Написание миграций схемы
-
Написание бизнес логики для API
-
Swagger. Написание документация и генерация документации.
-
Кто такой HTTP клиент. Утилиты Postman, curl, insomnia. Устанавливаем Postman.
-
Проверяем правильность работы API
-
Безумно полезные ссылки
Ранее мы уже познакомились с тем, что такое проект на Go: увидели структуру проекта, поработали с шаблонами, присоеденили базу данных, даже подняли ее как настоящие профессионалы и стали начинающими веб-разработчиками.
С таким багажом Вас вряд ли бы взяли на стажировку и сегодня мы напишем уже реальный аналог проекта.
Ваша задача — это написать единый сервис, в который будут обращаться все приложения компании для получения последних данных об изменениях курса акций.
Получив задание Вы сразу ринулись в бой, применяя имеющиеся знания, и написали за ночь проект на Go с шаблонами, к которому должны будут обращаться все устройства пользователей
Вы приносите свой проект тимлиду прямо на следующий день. Он смотрит с удивлением и говорит: "Здорово, что ты написал проект так быстро, но это не совсем то, что нужно. Как, например, телеграмм бот возьмет данные со страницы на HTML? Нужно писать отдельный парсер, чтобы получить из нее данные, а лишнее время и деньги. Необходимо провести дополнительное исследование".
Если подумать, то HTML нужен для передачи информации от компьютера(сайта) к человеку, а нам нужно решить задачу передачи информации от компьютера к компьютеру. То есть сделать такой сервис, куда бы приходил телеграмм бот(или мобильное приложение или сайт) задавал бы вопрос: "сколько стоят акции Apple сейчас?" на своем компьютерном, а потом на компьютерном получал бы ответ. Поэтому шаблоны тут не подойдут, они нужны для людей, а не для машин.
Теперь мы поняли, что нам не подходят шаблоны, ведь нам нужно создать решение для передачи данных от программы к программе. Для решения таких задач обычно используют API.
Определение API можно посмотреть здесь.
В нашем случае это будет выглядеть так:
То есть каждая из платформ приходит к нам в API за актуальной информацией о курсах акций. И это удобно, ведь нам не нужно писать программу для получения акций для каждой платформы отдельно, вместо этого можно написать один сервер, где будет вся информация. Это чем-то похоже на вызов функции в коде, только эта функция вызывается с помощью HTTP. Все, что нам осталось — это описать наше API, то есть задать набор правил, по которым все платформы будут общаться с нашим сервером.
Очень понятное объяснение дано в статье.
Нам осталось описать набор правил, которые станут нашим API. К счастью для этого есть REST — набор правил и рекомендаций для того, чтобы создать как можно более удобное API. Это как дизайн, но для компьютерных интерфейсов. Ведь нам, людям, удобнее читать информацию с сайта, который хорошо задизайнен и выглядит красиво. Также и машинам удобнее получать информацию с помощью API, который хорошо задизайнен. И REST — это как раз дизайн для нашего API.
Давайте представим, как может выглядеть архитектура нашего API для курса акций.
Для начала нужно понять, какие действия в целом с акциями можно делать:
- Получение списка всех акций, чтобы узнать какие акции есть у нас в системе
- Создание новой компании в системе, чтобы добавить новую компанию в нашу базу данных
- Получение стоимости акций конкретной компании, чтобы узнать стоимость акции не загржая огромного списка
- Изменение стоимости акций конкретной компании, чтобы поддерживать стоимость акций актуальными
- Удаление акции компании из базы, чтобы перестать отображать компанию если она, например, закрылась
В REST мы всё проектируем исходя из ресурса(модели). Способ, которым мы идентифицируем ресурс для предоставления, состоит в том, чтобы назначить ему URI — универсальный идентификатор ресурса. Перепишем тот же самый список сверху, но уже по REST:
- Получение списка всех акций: GET /stocks
- Создание новой компании в системе: POST ****/stocks/
- Получение стоимости акций конкретной компании: GET /stocks/1
- Изменение стоимости акций конкретной компании: PUT /stocks/1/
- Удаление акции компании из базы: DELETE /stocks/1/
Эту архитектуру мы показали тимлиду, утвердили ее и теперь преступаем к написанию приложения на Golang REST Framework (в нашем случае gin-gonic).
Пожалуйста, посмотрите дополнительные материалы в статье.
Определим структуру акции:
- uuid (аналог ID, почему именно uuid вы можете прочитать здесь: https://mareks-082.medium.com/auto-increment-keys-vs-uuid-a74d81f7476a)
- name (название акции)
- purchase_price (цена покупки )
- sale_price (цена продажи)
- count (число акций компании)
- company_name (название компании)
- inn (инн компании)
И определим ее в коде(не забывая раскаладывать все по пакетам, которые мы обсуждали во 2 ЛР):
type Stock struct {
UUID uint `sql:"type:uuid;primary_key;default:uuid_generate_v4()"`
Name string
PurchasePrice uint64
SalePrice uint64
Count uint64
CompanyName string
INN string
}
Для разграничения действий с ресурсами на уровне HTTP-методов и были придуманы следующие варианты:
- GET — получение ресурса
- POST — создание ресурса
- PUT — обновление ресурса
- DELETE — удаление ресурса
По умолчанию заходя на ресурс в браузере как мы делали ранее - мы делали GET запрос. Теперь же познакмомимся с ними всеми. В нашем фреймворке у объекта роутер есть реализации всех вышеперечисленных видов запроса, к примеру можем увидеть запрос DELETE:
В прочем, из того что могло бы оказаться для вас новым стоит рассмотреть запросы где параметры передаются внутри пути. Подробнее об этом вы можете узнать в документации фреймворка: https://github.com/gin-gonic/gin?ysclid=l8rr0pd0vx287684334#parameters-in-path.
func main() {
router := gin.Default()
// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
b := c.FullPath() == "/user/:name/*action" // true
c.String(http.StatusOK, "%t", b)
})
// This handler will add a new router for /user/groups.
// Exact routes are resolved before param routes, regardless of the order they were defined.
// Routes starting with /user/groups are never interpreted as /user/:name/... routes
router.GET("/user/groups", func(c *gin.Context) {
c.String(http.StatusOK, "The available groups are [...]")
})
router.Run(":8080")
}
В случае плохих ответов(н-р пользователь не ввел все данные или какие-то ввел не верно), стоит соблюдать следующие пункты:
- Ответ в json
{
"status": "fail",
"message": "invalid id, it must be >= 0"
}
- Соответсующий http status code(20* - ok, 50* - server errors)
Дальнейшие пункты, которые могут вызывать сложности, вы можете изучить в документации. В частности изучите вопрос удаления данных, полной выборки из таблицы и другие. Подробнее на https://gorm.io/docs/index.html
Swagger — это инструментарий, использующий спецификацию OpenAPI. Например, OpenAPIGenerator и SwaggerUI. Спецификация OpenAPI (openapi.json). Спецификация OpenAPI — это документ, описывающий возможности API.
Как выглядит свагер описание? Откройте файл merged.swagger.json и вы можете наглядно посмотреть.
Большинство IDE отлично интерпретируют свагер. Например Goland дает нам возможность параллельно смотреть исходники документации
и видеть визуальный результат. Сваггер можно описывать в json формате, а так же в yaml формате.
В проекте как правило такую документацию помещают в корень проекта в папку /docs
.
Сваггер можно написать вручную, однако когда над вашим проектом работают 5 разработчиков,
то кто-то обязательно забудет его дополнить, дополнив при этом еще один запрос новым параметром,
передав эту документацию вашим клиентам она уже станет не валидной.
Решить эту проблему можно несколькими путями:
- Фреймворк, который генерирует вам код на которым вы пишите сервер из swagger.
- Описание контракта для протоколов protobuf(.proto файлы), которые сгенерируют вам фрагмент сервера. А так же параллельно swagger документацию.(Более популярно в профессиональных коллективах)
- Генерировать сваггер из комментариев. (Мы будем использовтаь этот вариант)
Изучите документацию библиотеки для генерации сваггера и установите утилиту swag согласно документации: https://github.com/swaggo/swag
Создадим свою первую swagger документацию:
Добавим комментарии в файл main.go
package main
import (
"context"
"log"
"os"
"awesomeProject/internal/pkg/app"
)
// @title BITOP
// @version 1.0
// @description Bmstu Open IT Platform
// @contact.name API Support
// @contact.url https://vk.com/bmstu_schedule
// @contact.email [email protected]
// @license.name AS IS (NO WARRANTY)
// @host 127.0.0.1
// @schemes https http
// @BasePath /
func main() {
ctx := context.Background()
log.Println("Initializing server")
application, err := app.New(ctx)
if err != nil {
log.Println("cant create application")
os.Exit(2)
}
log.Println("Application start!")
application.Run()
log.Println("Application terminated!")
}
Добавим описание эндпоинта и вынесем его вне анонимной функции:
server.go
package app
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func (a *Application) StartServer() {
log.Println("Server start up")
r := gin.Default()
r.GET("/ping/:name", a.Ping)
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
log.Println("Server down")
}
type pingReq struct{}
type pingResp struct {
Status string `json:"status"`
}
// Ping godoc
// @Summary Show hello text
// @Description very very friendly response
// @Tags Tests
// @Produce json
// @Success 200 {object} pingResp
// @Router /ping/{name} [get]
func (a *Application) Ping(gCtx *gin.Context) {
name := gCtx.Param("name")
gCtx.String(http.StatusOK, "Hello %s", name)
}
Сгенерируем сваггер документацию:
$ swag init -g cmd/awesomeProject/main.go
2022/10/02 23:16:10 Generate swagger docs....
2022/10/02 23:16:10 Generate general API Info, search dir:./
2022/10/02 23:16:10 warning: failed to get package name in dir: ./, error: execute go list command, exit status 1, stdout:, stderr:no Go files in /Users/maxim-konovalov/VK/Cloud/awesomeProject
2022/10/02 23:16:10 Generating app.pingResp
2022/10/02 23:16:10 create docs.go at docs/docs.go
2022/10/02 23:16:10 create swagger.json at docs/swagger.json
2022/10/02 23:16:10 create swagger.yaml at docs/swagger.yaml
Как видим документация сгенерировалась успешно:
$ ls -l ./docs
total 24
-rw-r--r-- 1 maxim-konovalov staff 2801 Oct 2 23:16 docs.go
-rw-r--r-- 1 maxim-konovalov staff 1334 Oct 2 23:16 swagger.json
-rw-r--r-- 1 maxim-konovalov staff 682 Oct 2 23:16 swagger.yaml
Откроем и посмотрим результат, убедимся что все сгенерировалось согласно нашим комментариям:
HTTP клиент - инцициатор общения по протоколу по http, а так же инструменты которые позволяют ему это сделать. Когда мы проверяли запросы в браузере - наш браузер является http клиентом. Правда браузер как клиент достаточно спецефичен. Он по определению создан для другого - он получает файлы фронтенда, которые уже внутри JS кода делают свои запросы на ваш сервер. Чтобы иммитировать все возможные http запросы есть множество утилит. Самые популярные из них:
- Postman (имеет огромную функциональность, которая поможет в тестировании и шаринге ваших запросов в команде, но самая продвинутая платная)
- Insomnia(менее функциональный для команлы, но все так же может делать любые запрсоы)
- curl (http клиент из консоли, как правильно обмениваясь запросами - обмениваются curl запросами, поскольку это просто строка, в дальнейшем вы можете его интерпретировать в postman или другом клиенте). На самом деле, curl - это больше чем просто утилита командной строки для Linux или Windows. Это набор библиотек, в которых реализуются базовые возможности работы с URL страницами и передачи файлов. Библиотека поддерживает работу с протоколами: FTP, FTPS, HTTP, HTTPS, TFTP, SCP, SFTP, Telnet, DICT, LDAP, а также POP3, IMAP и SMTP. Она отлично подходит для имитации действий пользователя на страницах и других операций с URL адресами.
Проверять правильность выполнения ЛР будем через утилиту Postman. Для создания отчета создайте Postman коллекцию запросов. Подробнее документацию вы можете почитать на https://learning.postman.com/docs/publishing-your-api/documenting-your-api/ В конечном итоге у вас должна появиться коллекция запросов, которыми вы сможете провести ручное тестирование вашего API.
Составьте план ручного тестирования. Убедитесь что все данные возвращаются в формате JSON. Проверьте краевые случаи(id = -1, id=999999999999, id = """) и корректность возвращаемого ответа-ошибки.
Проверьте что у вас присуствуют правильные коды ошибок(у вас обязательно должно быть больше 2 кодов ответа):
- 400 Bad Request — сервер обнаружил в запросе клиента синтаксическую ошибку. Появился в HTTP/1.0.
- 200 OK — успешный запрос. Если клиентом были запрошены какие-либо данные, то они находятся в заголовке и/или теле сообщения. Появился в HTTP/1.0.
- 201 Created — в результате успешного выполнения запроса был создан новый ресурс. Сервер может указать адреса (их может быть несколько) созданного ресурса в теле ответа, при этом предпочтительный адрес указывается в заголовке Location. Серверу рекомендуется указывать в теле ответа характеристики созданного ресурса и его адреса, формат тела ответа определяется заголовком Content-Type. При обработке запроса новый ресурс должен быть создан до отправки ответа клиенту, иначе следует использовать ответ с кодом 202. Появился в HTTP/1.0.
- 500 Internal Server Error — любая внутренняя ошибка сервера, которая не входит в рамки остальных ошибок класса. Появился в HTTP/1.0.
Результат выполненной работы:
- Работующее API для вашего сервера по вашей теме в сооствествии с запросами выше.
- Документация swagger openapi
- Postman коллекция запросов с правильными запросами и запросами на тестирования(с плохими ответами)
- Консистентность статуса ответа и записи в БД. Ваш сервис не может ответить ок, не изменив базу данных в конкретной задаче.
- Документация Postman - https://learning.postman.com/docs/publishing-your-api/documenting-your-api/
- Как пользоваться curl - https://losst.ru/kak-polzovatsya-curl?ysclid=l91dvjg4xg818007414
- Документация gin-gonic - https://github.com/gin-gonic/gin?ysclid=l8rr0pd0vx287684334
- Документация gorm - https://gorm.io/docs/
- Список кодов состояния http - https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP