Делаем Django на Go (ну почти)
Intro⌗
Go - классный язык программирования у которого есть все шансы обеспечить конкуренцию Python. Но, до недавнего времени писать сервисы на нем было отдельным страданием т.к. язык “молодой” и еще не оброс всякими фреймворками. Сейчас же ситуация изменилась в лучшую сторону и можно сделать шустрый rest-backend с относительно небольшими временными затратами.
Придумаем себе проблему⌗
Чтобы показать насколько нынешний Go подходит для создания rest-приложений, можно сконцентрироваться на решении задачи - бекенд для блога. Которая в достаточной степени раскроет возможности языка и отдельных библиотек. Что нам нужно? Очевидно, нужна абстракция над моделями данных, в идеале - ORM. Затем нужна библиотека которая будет обслуживать ввод/вывод данных по REST + поддержка аутентификации (мидлварей). Итак, приступим
Модели данных и ORM⌗
Для построения структуры используется модуль GORM. Дает очень удобный функционал доступа к данным.
Настройка⌗
Для примера будем использовать PostgreSQL, но для других SQL движков настройка будет +/- похожей. Чтобы сделать настройку реюзабельной, добавить указатель на структуру gorm.DB
var ORM *gorm.DB
в которую добавим коннектор
func connectDB() error {
dsn := "host=<DB ADDRES> port=<DB PORT> user=<DB USER> password=<DB PASSWORD> dbname=<DB NAME> TimeZone=UTC"
models.ORM, dbErr = gorm.Open(postgres.Open(dsn), &gorm.Config{})
return dbErr
}
функцию можем вызвать хоть в init-файле, хоть в любом месте где будем готовы подключиться к БД. После, можем использовать переменную ORM для взаимодействия с базой через абстракции.
Модели⌗
В Go для определения модели данных есть крутая абстракция - struct. Заведем несколько структур которые необходимы для хранения
type Post struct {
ID int64 `json:"id" gorm:"primaryKey;->"`
Slug string `json:"slug" gorm:"unique"`
Title string `json:"title" gorm:"type:varchar(100)"`
Text string `json:"text", gorm:"type:text"`
CreatedAt uint64 `json:"created_at"`
UpdatedAt uint64 `json:"updated_at"`
IsPublished bool `json:"is_published"`
}
type Tag struct {
ID int64 `json:"id" gorm:"primaryKey;->"`
Name string `json:"name" gorm:"unique"`
}
type PostTag struct {
ID int64 `gorm:"primaryKey;->"`
PostId int64 `gorm:"index:idx_post_tag,unique"`
TagId int64 `gorm:"index:idx_post_tag,unique"`
}
Как видно из примера выше, вся служебная информация необходимая информация, для того чтобы ORM понимала с каким типом поля она работает, вносится в теги структуры. Чтобы не создавать таблицы вручную, можно передать структуры в мигратор GORM
err = models.ORM.AutoMigrate(
&models.Post{},
&models.Tag{},
&models.PostTag{})
Создание⌗
Для удобного создания поста в блоге, можем добавить метод к соответствующей структуре
func (post *Post) Create() error {
post.CreatedAt=uint64(time.Now().UnixNano())
qs := ORM.Create(&post)
return qs.Error
}
таким образом, в представлении нам понадобится только заполнить структуру и вызвать метод Create, который провернет все необходимые изменения с данными прежде чем их сохранить.
Обновление⌗
Обновление, по сути это тоже создание, только у нас известен ID (или слаг) поста
func (post *Post) Update() error {
post.UpdatedAt=uint64(time.Now().UnixNano())
qs := ORM.Where("id = ?", post.ID).Update(&post)
return qs.Error
}
Удаление⌗
И так же просто делаем удаление
func (post *Post) Delete() error {
qs := ORM.Where("id = ?", post.ID).Delete(&post)
return qs.Error
}
Делаем REST⌗
Для REST в Go тоже есть крутой фреймворк - GIN.
Настройка сервера⌗
Для начала нам надо создать сервер, который будет “слушать” определенный порт. Для этого создаем файлик server.go c примерно таким содержимым
package main
func main(){
}
Так компилятор поймет, что это основная точка входа и будет строить зависимости от этого файла. Далее нам надо добавить роутер, код который отвечает за соотнесение функции к запрашиваемому пути. Например, так
func Api(router *gin.Engine) *gin.RouterGroup {
v1 := router.Group("/v1")
{
v1.GET("/post/list", v1_views.PostList)
}
return v1
Таким образом, создаем группу эндпоинтов объединённых по версии, в нашем случае, v1. Внутри этой группы добавляем путь для получения списка постов. Функция, которая сделает представление, будет выглядеть так
func PostList(c *gin.Context) {
offset, offsetErr := strconv.Atoi(c.Query("offset"))
if offsetErr != nil {
offset = 0
}
limit, limitErr := strconv.Atoi(c.Query("limit"))
if limitErr != nil {
limit = 30
}
var posts []Post
qs := ORM.Where("is_published = true").Limit(limit).Offset(offset).Find(&posts)
c.JSON(200, posts)
}
Тут мы делаем не только выборку всех опубликованных постов, но и ограничиваем ее лимитом и добавляем смещение. Так, же можно модифицировать вывод и добавить структуру для отображения total и текущих параметров, но в данном случае будет только список постов.
Раз уж все зависимости готовы, пора вернуться к функции main. Добавим код, который будет отвечать за сервер
func main(){
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
apiv1.Api(router)
err = router.Run("127.0.0.1:8080")
if err != nil {
panic(err)
}
}
Как видно, перед запуском сервера я добавил пару middleware, для логирования и возращение 500 на любую panic-ошибку. После этого подключаем наше v1-api к основному роутеру и запускаем сервер по адресу “127.0.0.1:8080”. Теперь почти все готово к первому билду, осталось только синхронизировать зависимости, выполняем такую команду
$ go mod tidy && go mod verify && go mod tidy
и собираем
$ go build -tags=nomsgpack -o server .
после этого в директории появится бинарный файл server выполнив который увидим лог запуска и можем пробовать открыть в браузере http://127.0.0.1:8080/v1/post/list
Создание постов⌗
Для создания записей нам нужно добавить 2 вещи:
- Аутентификацию юзера
- Перенос данных из JSON -> модель БД
Аутентификация⌗
Чтобы проверить, что пользователь который к нам пришел, является тем кому можно создавать посты, надо сделать такую функцию
func AppAuth() gin.HandlerFunc {
return func(context *gin.Context) {
user, password, hasAuth := context.Request.BasicAuth()
if hasAuth && user == "user" && password == "qwerty" {
context.Set("isAuth", true)
} else {
context.Set("isAuth", false)
}
}
}
такая функция выставит нам флаг по которому будет понятно прошел юзер аутентификацию или нет. Этот флаг мы проверим в “ручке” на создание поста
Создание поста⌗
func PostCreate(c *gin.Context) {
isAuth := c.MustGet("isAuth").(bool)
if !c.isAuth {
c.JSON(403, "unauthorized")
return
}
var post Post
err := c.BindJSON(&port)
if err != nil {
c.JSON(400, fmt.Sprintf("wrong request: %v", err))
return
}
err = post.Create()
if err != nil {
c.JSON(500, fmt.Sprintf("internal error: %v", err))
return
}
c.JSON(200, post)
}
Если все данные были переданы правильно, наш пост появится в списке.
Extra⌗
Разумеется в данном очерке представлены далеко не все возможности GORM и Gin. Например, дополнительно можно добавить миддлварь для CORS или обернуть вывод в структуру с красивым логированием через logrus. Простор модификаций предложенных примеров ограничивается лишь воображением читателя :)