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. Простор модификаций предложенных примеров ограничивается лишь воображением читателя :)