Официальные сайты с инструкциями и документациями: beego , gorm
При написании backend на Go обычно используют готовые библиотеки, одни из популярных сейчас - Chi
,
Gin
и Gorilla Mux
.
В данной статье мы рассмотрим менее популярную библиотеку - BeeGo
.
Напишем RESTful API, соберем документацию в Swagger
, добавим тесты и свяжем с PostgreSQL
базой при помощи популярной Gorm
.
Написанный микро-сервис будет использоваться для авторизации пользователей в системе по средством кода из SMS сообщения.
Beego
представляет из себя мощный набор инструментов.
При создании проекта Beego
создает структуру дирректорий (описана ниже) и даже файлы примеров.
Таким образом, при создании проекта, мы сразу получаем готовую модель API
сервиса с рабочим примером, который можно исправить под свои нужды.
Структура созданного проекта в Beego
имеет MVC
шаблон разделения данных.
MVC
- Model-View-Controller
(«Модель-Представление-Контроллер», «Модель-Вид-Контроллер») —
схема разделения данных приложения и управляющей логики на три отдельных компонента: модель, представление и контроллер —
таким образом, что модификация каждого компонента может осуществляться независимо.
Модель (Model
) предоставляет данные и реагирует на команды контроллера, изменяя своё состояние.
Представление (View
) отвечает за отображение данных модели пользователю, реагируя на изменения модели.
Контроллер (Controller
) интерпретирует действия пользователя, оповещая модель о необходимости изменений.
Gorm
- это реализация доступа к базе данных на языке GoORM
- объектно-реляционное отображение, или преобразование — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования.Beego
- Свободно распостаняемый фреймворк для разработки и сборки ваших приложений на языке GoSwagger
- Позволяет автоматически создавать клиентские библиотеки API
Установка
Beego
go get -u github.com/beego/beego/v2@latest
Для импорта используется:
beego "github.com/beego/beego/v2/server/web"
Beego tools
go get -u github.com/beego/bee
Теперь необходимо добавить $GOPATH/bin
в переменную окружения PATH
:
export PATH=$GOPATH/bin:$PATH
Если команда bee version
возвращает ошибку, что bee
не найден, тогда выполните установку:
go install github.com/beego/bee
Gorm
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
Создаем API структуру
bee api sms
В результате получаем готовую структуру из дирректорий с файлами примеров.
Описание базовых и дополнительных директорий и файлов
Автоматически созданные директории
- routers
- controllers
- swagger
- models
- tests
Создаются дополнительно
- conf
- db
- lib/sms_service
Описание структуры
├── conf
│ └── app.conf - конфигурационный файл, содержащий в основном переменные для подключения к БД
├── controllers
│ └── sms.go - контроллер, содержит код для автоматической генерации документации в формате swagger и описание точек входа.
├── db
│ ├── conn.go - подключение и взаимодействия с базой данных.
│ ├── migration.go - автоматический перенос схемы, для поддержания в актуальном состоянии.
│ └── serializer.go - JSON serializer интерфейс и функции его реализации.
├── lib
│ └── sms_service
│ └── sms_service.go - осуществляет взаимодействие запросов ендпоинтов с базой данных.
├── models
│ ├── entity
│ │ ├── common.go - содержит структуру базовых полей для БД (ID, время создания\изменения\удаления)
│ │ └── sms.go - базовая структура с укаазанием размера полей и кодом, который отобразится как пример в swagger-документации.
│ ├── request
│ │ └── sendsms.go - обработка входящих запросов и функция для валидации.
│ └── response
│ └── sms.go - формирование ответа.
├── routers
│ └── router.go - содержит структуру HTTP-путей.
├── swagger - дерриктория содержащая swagger, внутри нам необходимо исправить только 1 файл.
└── tests
└── sms_test.go - содержит тесты для точек входа.
Прописываем базовый путь до контроллера в router.go
func init() {
ns := beego.NewNamespace("/v1",
beego.NSNamespace("/sms",
beego.NSInclude(
&controllers.SMSController{},
),
),
)
beego.AddNamespace(ns)
}
Указываем путь до нашего swagger.json
Открываем swagger/index.html
и для url указыввем путь к swagger.json
:
const ui = SwaggerUIBundle({url: "./swagger.json", ....
Задаем переменные в конфигурационном файле
appname = sms
httpport = 8080
dbUser = ${DB_USER||postgres}
dbPass = ${DB_PASSWORD||postgres}
dbHost = ${DB_HOST||localhost}
dbName = ${DB_NAME||sms}
Написание обработчиков
Создаем основные структуры в models/entity
common.go
- базовая модель для БД. Первичный ключ и общие поля.
type BaseModel struct {
ID string `gorm:"primary_key;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt *time.Time `sql:"index" json:"-"`
}
sms.go
- описываем основную структуру для SMS с описанием для документации свагера
type SMS struct {
BaseModel
Phone string `gorm:"column:phone" json:"phone" example:"+79991122233"` // Телефон пользователя
Username string `gorm:"size:255; column:username" json:"username" example:"test_user"` // username пользователя
FIO string `gorm:"size:255; column:fio" json:"fio" example:"Ivanov Ivan Ivanovich"` // ФИО пользователя
Message string `gorm:"size:255; column:message" json:"message" example:"232-876"` // Код для входа
}
Создаем структуры для запросов в models/request/sendsms.go
type SendSMS struct {
Phone string `json:"phone" example:"+79995553311"` // Телефон пользователя
Username string `json:"username" example:"test_user"` // username пользователя
FIO string `json:"fio" example:"Ivanov Ivan Ivanovich"` // ФИО пользователя
Message string `json:"message" example:"232-876"` // Код для входа
}
Так же добавим методы для конвертации (паттерн адаптер )
func (r SendSMS) ConvertToEntity() entity.SMS {
return entity.SMS{
Phone: r.Phone,
Username: r.Username,
FIO: r.FIO,
Message: r.Message,
}
}
Добавляем валидацию входящего запроса в models/request/sendsms.go
Для провекрки будем использовать уже встроенные инструмент Beego
- validation
,
а также regexp
для проверки мобильного номера телефона (опционально, так как номер телефона тоже можно проверить через validation
).
Required
- проверяем, что переменная не пустая.
MaxSize
- проверяем, что максимальный размер не более 255 символов.
Для номера телефона - сравниваем полученные данные с регулярным выражением (находится в конфиге).
func (sms SendSMS) Validate() error {
valid := validation.Validation{}
valid.Required(sms.Username, "username")
valid.MaxSize(sms.Username, 255, "usernameMax")
valid.Required(sms.FIO, "FIO")
valid.MaxSize(sms.FIO, 255, "FIOMax")
valid.Required(sms.Message, "message")
valid.MaxSize(sms.Message, 255, "messageMax")
if valid.HasErrors() {
for _, err := range valid.Errors {
return errors.New(err.Key + err.Message)
}
}
phoneRegexp, _ := beego.AppConfig.String("phoneRegexp")
re := regexp.MustCompile(phoneRegexp)
if ok := re.MatchString(sms.Phone); !ok {
return errors.New("invalid phone number format or length")
}
return nil
}
Создаем структуры для ответов в models/response/sms.go
type SMSes []SMS
type SMS struct {
ID string `json:"ID" example:"1f507148-c71d-4d64-b376-b9207370f3c6"` //ID поля
Phone string `json:"phone" example:"+79991122233"` // Телефон пользователя
Username string `json:"username" example:"test_user"` // username пользователя
FIO string `json:"fio" example:"Ivanov Ivan Ivanovich"` // ФИО пользователя
Message string `json:"message" example:"232-876"` // Код для входа
}
И адаптеры для конвертации - дальнейшая работа осуществляется с entity.SMS
func ConvertSMSToModel(dbSMS entity.SMS) SMS {
return SMS{
ID: dbSMS.ID,
Phone: dbSMS.Phone,
Username: dbSMS.Username,
FIO: dbSMS.FIO,
Message: dbSMS.Message,
}
}
func ConvertSMSesToModel(dbSMSes []entity.SMS) SMSes {
var SMSesResponse SMSes
for _, dbSMS := range dbSMSes {
SMSesResponse = append(SMSesResponse, ConvertSMSToModel(dbSMS))
}
return SMSesResponse
}
Пишем обработчики в controllers/sms.go
Объявляем структуру для beego
контроллера:
type SMSController struct {
beego.Controller
}
Для каждой uri
должны быть комментарии, которые автоматически собираются beego
в swagger
в документацию.
Пример:
// @Title SendSMS
// @Description send SMS
// @Param body body request.SendSMS true "body for SMS content"
// @Success 201 created
// @Failure 400 bad request
// @Failure 500 internal server error
// @router / [post]
Обработка запросов
Принимаем JSON
запрос и парсим в request.SendSMS
, проверяем (функция валидации выше в models/request/sendsms.go
), выполняем и возвращаем HTTP статус ответ и,
если требуется, тело или ошибку.
func (c *SMSController) SendSMS() {
var sms request.SendSMS
err := json.Unmarshal(c.Ctx.Input.RequestBody, &sms)
if err != nil {
c.Ctx.Output.SetStatus(http.StatusBadRequest)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
err = sms.Validate()
if err != nil {
c.Ctx.Output.SetStatus(http.StatusBadRequest)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
err = smsservice.Instance.SendSMS(sms)
if err != nil {
c.Ctx.Output.SetStatus(http.StatusInternalServerError)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
c.Ctx.Output.SetStatus(http.StatusCreated)
}
Функция для записи готова.
Теперь напишем функцию для вывода всех существующих записей. Используем функцию GetSMSes
который объявляли в sms_service.go
.
Читаем все записи в слайс и формируе ответ используя функцию ConvertSMSesToModel
из /response/sms.go
// @Title GetSMSes
// @Description Получить SMS
// @Success 200 {object} response.SMSes
// @Failure 404 not found
// @Failure 500 internal server error
// @router / [get]
func (c *SMSController) GetSMSes() {
sms, err := smsservice.Instance.GetSMSes()
if err != nil {
c.Ctx.Output.SetStatus(http.StatusInternalServerError)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
if len(sms) == 0 {
c.Ctx.Output.SetStatus(http.StatusNotFound)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
c.Ctx.Output.SetStatus(http.StatusOK)
c.Data["json"] = sms
c.ServeJSON()
}
Напишем функцию для вывода одной записи по ID
// @Title GetSMSByID
// @Description Получить sms по id
// @Param id path string true "ID sms"
// @Success 200 {object} response.SMS
// @Failure 404 not found
// @Failure 500 internal server error
// @router /:id [get]
func (c *SMSController) GetSMSByID(id string) {
sms, err := smsservice.Instance.GetSMSByID(id)
if errors.Is(err, gorm.ErrRecordNotFound) {
c.Ctx.Output.SetStatus(http.StatusNotFound)
return
}
if err != nil {
c.Ctx.Output.SetStatus(http.StatusInternalServerError)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
c.Ctx.Output.SetStatus(http.StatusOK)
c.Data["json"] = sms
c.ServeJSON()
}
И функцию для удаления записи по ID
// @Title DeleteUser
// @Description Удалить sms
// @Param id path string true "ID sms"
// @Success 204
// @Failure 500 internal server error
// @router /:id [delete]
func (c *SMSController) DeleteSMS(id string) {
err := smsservice.Instance.DeleteSMS(id)
if err != nil {
c.Ctx.Output.SetStatus(http.StatusInternalServerError)
c.Ctx.Output.Body([]byte(err.Error()))
return
}
c.Ctx.Output.SetStatus(http.StatusOK)
}
Пишем сервис для взаимодействий с БД lib/sms_service/sms_service.go
Объявляем интерфейс для взаимодействия с backend (в нашем случае - PostgreSQL
)
type Service interface {
GetSMSes() (response.SMSes, error)
GetSMSByID(smsID string) (response.SMS, error)
SendSMS(sms request.SendSMS) error
DeleteSMS(smsID string) error
}
Создаем переменную типа интерфейс Service
, который объявляли ранее и присваиваем структуру smsService
,
которая содержит поле с указателем на структуру базы данных и добавляем поле с функцией db.GetBD()
для подключения к нашей БД.
var Instance Service = smsService{
db: db.GetDB(),
}
type smsService struct {
db *gorm.DB
}
Пишем функцию для записи в базу данных. Функция пишет принятые данные в сообтветствующие поля базы данных.
func (s smsService) SendSMS(sms request.SendSMS) error {
dbSMS := sms.ConvertToEntity()
err := s.db.Create(&dbSMS).Error
if err != nil {
return errors.Wrapf(err, "Ошибка создания SMS %s", sms.Username)
}
return nil
}
Пишем функцию для чтения всех существующих записей в БД
func (u smsService) SendSMS(sms request.SendSMS) error {
dbSMS := sms.ConvertToEntity()
err := u.db.Create(&dbSMS).Error
if err != nil {
return errors.Wrapf(err, "Ошибка создания SMS %s", sms.Username)
}
return nil
}
Функция для чтения одной записи, запись находится по ID
func (u smsService) GetSMSByID(smsID string) (response.SMS, error) {
sms := entity.SMS{BaseModel: entity.BaseModel{ID: smsID}}
err := u.db.Debug().First(&sms).Error
if err != nil {
return response.SMS{}, errors.Wrapf(err, "Ошибка чтения SMS %v из базы", smsID)
}
return response.ConvertSMSToModel(sms), nil
}
Функция для удаления записи из backend’а по ID
func (u smsService) DeleteSMS(smsID string) error {
err := u.db.Delete(&entity.SMS{}, "id = ?", smsID).Error
if err != nil {
return errors.Wrapf(err, "Ошибка удаления SMS %s", smsID)
}
return nil
}
Тестирование
Опишем тесты в tests/sms_test.go
Необходимо создать тесты, по одному на проверку каждой функции в API
- отправка СМС, получения всех СМС, получение одной СМС по ID
,
удаление СМС и проверка возврата ошибки в случае получения пустой СМС.
func init() {
_, file, _, _ := runtime.Caller(0)
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
beego.TestBeegoInit(apppath)
}
var id string
var expectedSMS string = `{"fio": "Ivanov Ivan Ivanovich","message": "232-876","phone": "+79991122233","username": "test_user"}`
// TestSendSMS send SMS
func TestSendSMS(t *testing.T) {
body := bytes.NewReader([]byte(expectedSMS))
r, _ := http.NewRequest("POST", "/v1/sms", body)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
logs.Info("testing", "TestSendSMS", fmt.Sprintf("Code[%d]\n", w.Code))
Convey("Subject: Test Station Endpoint\n", t, func() {
Convey("Status Code Should Be 201", func() {
So(w.Code, ShouldEqual, 201)
})
Convey("The Result Should Be Empty", func() {
So(w.Body.Len(), ShouldEqual, 0)
})
})
}
// TestGetSMSes get SMS list
func TestGetSMSes(t *testing.T) {
r, _ := http.NewRequest("GET", "/v1/sms", nil)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
logs.Info("testing", "TestGetSMSes", fmt.Sprintf("Code[%d]\n", w.Code))
Convey("Subject: Test Station Endpoint\n", t, func() {
Convey("Status Code Should Be 200", func() {
So(w.Code, ShouldEqual, 200)
})
Convey("The Result Should Not Be Empty", func() {
So(w.Body.Len(), ShouldBeGreaterThan, 0)
})
})
var sms response.SMSes
err := json.Unmarshal(w.Body.Bytes(), &sms)
if err != nil {
t.Fatal(err)
}
id = sms[0].ID
}
func TestGetSMSByID(t *testing.T) {
r, _ := http.NewRequest("GET", "/v1/sms/"+id, nil)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
logs.Info("testing", "TestGetSMSByID", fmt.Sprintf("Code[%d]\n", w.Code))
Convey("Subject: Test Station Endpoint\n", t, func() {
Convey("Status Code Should Be 200", func() {
So(w.Code, ShouldEqual, 200)
})
Convey("The Result Should Not Be Empty", func() {
So(w.Body.Len(), ShouldBeGreaterThan, 0)
})
})
var sms response.SMS
var expected response.SMS = response.SMS{
ID: id,
}
err := json.Unmarshal(w.Body.Bytes(), &sms)
if err != nil {
t.Fatal(err)
}
if expected.ID != sms.ID {
t.Fatalf("TestGetSMSByID: ID mismatch; got %s; want %s", sms.ID, expected.ID)
}
}
// TestDeleteSMS Delete SMS by ID
func TestDeleteSMS(t *testing.T) {
r, _ := http.NewRequest("DELETE", "/v1/sms/"+id, nil)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
logs.Info("testing", "TestDeleteSMS", fmt.Sprintf("Code[%d]\n", w.Code))
Convey("Subject: Test Station Endpoint\n", t, func() {
Convey("Status Code Should Be 204", func() {
So(w.Code, ShouldEqual, 204)
})
Convey("The Result Should Be Empty", func() {
So(w.Body.Len(), ShouldEqual, 0)
})
})
}
// TestGetEmptySMSes get empty SMS list
func TestGetEmptySMSes(t *testing.T) {
r, _ := http.NewRequest("GET", "/v1/sms", nil)
w := httptest.NewRecorder()
beego.BeeApp.Handlers.ServeHTTP(w, r)
logs.Info("testing", "TestGetEmptySMSes", fmt.Sprintf("Code[%d]\n", w.Code))
Convey("Subject: Test Station Endpoint\n", t, func() {
Convey("Status Code Should Be 404", func() {
So(w.Code, ShouldEqual, 404)
})
Convey("The Result Should Not Be Empty", func() {
So(w.Body.Len(), ShouldBeGreaterThan, 0)
})
})
}
Запуск тестов
go test tests/sms_test.go