Команда Go for Devs подготовила перевод статьи по созданию RESTful API на Go с использованием фреймворка Gin. Всего за несколько шагов вы напишете простой веб-сервис, который умеет возвращать список джазовых альбомов, добавлять новые и находить альбом по ID. Отличный старт для знакомства с Gin.


В этой статье рассматриваются основы создания RESTful веб-сервисов на Go с использованием веб-фреймворка Gin.

Gin упрощает многие задачи при создании веб-приложений, включая веб-сервисы. В этом учебнике вы будете использовать Gin для маршрутизации запросов, извлечения их параметров и формирования JSON-ответов.

В ходе работы вы создадите RESTful API-сервер с двумя эндпоинтами. Примером послужит проект — хранилище данных о винтажных джазовых пластинках.

Предварительные требования

  • Установленный Go версии 1.16 или новее. Инструкции по установке см. в разделе Installing Go.

  • Инструмент для редактирования кода. Подойдёт любой текстовый редактор.

  • CLI. Go отлично работает в любом терминале Linux и Mac, а также в PowerShell или cmd на Windows.

  • Утилита curl. На Linux и Mac она обычно уже установлена. В Windows она включена начиная с Windows 10 Insider build 17063. Для более ранних версий Windows её может потребоваться установить вручную.

Проектирование API-эндпоинтов

Вы будете создавать API, который предоставляет доступ к магазину, торгующему винтажными виниловыми пластинками. Поэтому необходимо предусмотреть эндпоинты, через которые клиент сможет получать список альбомов и добавлять новые.

Разработка API обычно начинается с проектирования эндпоинтов. Пользователям вашего API будет гораздо проще работать с ним, если эндпоинты будут интуитивно понятными.

Вот эндпоинты, которые мы сегодня создадим:

/albums

  • GET — получить список всех альбомов в формате JSON.

  • POST — добавить новый альбом, переданный в формате JSON.

/albums/:id

  • GET — получить данные об альбоме по его ID в формате JSON.

Далее вы создадите папку для кода.

Создание директории для кода

Для начала создайте проект, в котором будет располагаться ваш код.

Откройте командную строку и перейдите в домашний каталог.

На Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

Через командную строку создайте директорию для кода с именем web-service-gin:

$ mkdir web-service-gin
$ cd web-service-gin

Создайте модуль, в котором вы сможете управлять зависимостями.

Выполните команду go mod init, указав путь для модуля, в котором будет находиться ваш код:

$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

Эта команда создаст файл go.mod, в котором будут перечисляться все добавленные зависимости для их отслеживания. Подробнее о выборе имени модуля и путях см. в разделе Managing dependencies.

Далее вы спроектируете структуры данных для работы с информацией.

Подготовка данных

Чтобы упростить учебный пример, данные будут храниться в памяти. В реальном API обычно используется база данных.

Имейте в виду: при хранении в памяти набор альбомов будет теряться каждый раз при остановке сервера и создаваться заново при запуске.

Напишем код

В текстовом редакторе создайте файл main.go в каталоге web-service. В этот файл вы будете писать код на Go.

В начало main.go вставьте объявление пакета:

package main

Самостоятельная программа (в отличие от библиотеки) всегда находится в пакете main.

Ниже объявления пакета вставьте определение структуры album. Она понадобится для хранения данных об альбомах в памяти.

Теги структуры, например json:"artist", задают имена полей при сериализации структуры в JSON. Без тегов в JSON попали бы имена с заглавной буквы — стиль, менее привычный для JSON.

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

Под определением структуры вставьте срез album с начальными данными:

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

Далее вы напишете код для первого эндпоинта.

Написание обработчика для возврата всех элементов

Когда клиент делает запрос GET /albums, нужно вернуть все альбомы в формате JSON.

Для этого необходимо:

  • реализовать логику формирования ответа;

  • сопоставить путь запроса с этой логикой.

Обратите внимание: порядок добавления кода обратный тому, как он выполняется во время работы программы — сначала вы добавляете зависимости, затем код, который их использует.

Напишем код

Под определением структуры из предыдущего шага вставьте следующий код, который будет возвращать список альбомов:

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

В этом коде:

  • Создаётся функция getAlbums, принимающая параметр gin.Context. Имя функции можно выбрать любое — Gin и Go не требуют определённого формата.

  • gin.Context — ключевая часть фреймворка Gin. Она содержит данные запроса, умеет валидировать и сериализовать JSON и многое другое. (Несмотря на схожее название, это другой пакет, не связанный со встроенным в Go context.)

  • Вызов Context.IndentedJSON сериализует структуру в JSON и добавляет её в ответ.

  • Первый аргумент функции — HTTP-код состояния, который вы хотите вернуть клиенту. Здесь используется константа StatusOK из пакета net/http, означающая 200 OK.

Обратите внимание: вместо Context.IndentedJSON можно вызвать Context.JSON, чтобы отправить более компактный JSON. Но в отладке форматированный вариант удобнее, а разница в размере обычно несущественна.

В начале файла main.go, сразу под объявлением среза albums, вставьте следующий код, чтобы привязать обработчик к пути эндпоинта:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

Этот код устанавливает связь между функцией getAlbums и запросами к эндпоинту /albums.

В нём выполняются такие действия:

  • Инициализируется роутер Gin с помощью Default().

  • С помощью метода GET связываются HTTP-метод GET и путь /albums с функцией-обработчиком.
    Обратите внимание: вы передаёте имя функции getAlbums, а не результат её вызова. Если бы вы написали getAlbums(), функция выполнилась бы сразу.

  • Вызов Run привязывает роутер к http.Server и запускает сервер.

Вверху файла main.go, сразу под объявлением пакета, импортируйте необходимые пакеты для поддержки этого кода:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

Сохраните файл main.go.

Запуск кода

Теперь нужно подключить модуль Gin как зависимость.

В командной строке выполните команду go get, чтобы добавить модуль github.com/gin-gonic/gin в зависимости вашего проекта. Аргумент . означает «получить зависимости для кода в текущем каталоге».

$ go get .
go get: added github.com/gin-gonic/gin v1.7.2

Go найдёт и загрузит зависимость в соответствии с импортом, который вы добавили.

Теперь в каталоге с файлом main.go выполните команду для запуска кода. Аргумент . снова означает «запустить код из текущего каталога»:

$ go run .

После этого у вас будет работающий HTTP-сервер, готовый принимать запросы.

В новом окне командной строки выполните запрос к сервису с помощью curl:

$ curl http://localhost:8080/albums

Команда должна вывести данные, которые вы заранее добавили в сервис:

[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    }
]

Поздравляем, вы запустили API! В следующем разделе вы создадите ещё один эндпоинт для обработки POST-запроса и добавления нового элемента.

Написание обработчика для добавления нового элемента

Когда клиент отправляет POST-запрос на /albums, нужно добавить альбом, переданный в теле запроса, к существующим данным.

Для этого потребуется:

  • реализовать логику добавления нового альбома в список;

  • добавить код для маршрутизации POST-запроса к этой логике.

Напишем код

Сразу после блока import вставьте следующий код (чаще всего такие функции размещают в конце файла, но Go не требует определённого порядка объявления):

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

В этом коде:

  • используется Context.BindJSON, чтобы преобразовать тело запроса в структуру newAlbum;

  • новый альбом добавляется в срез albums;

  • в ответ возвращается статус 201 Created и JSON с данными добавленного альбома.

Теперь измените функцию main, чтобы добавить обработку POST-запросов:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

В этом коде:

  • метод POST по пути /albums связывается с функцией postAlbums.

В Gin можно привязывать обработчики к комбинациям «HTTP-метод + путь». Таким образом, один и тот же путь может иметь разные обработчики для GET, POST и других методов.

Запуск кода

Если сервер из предыдущего шага всё ещё запущен, остановите его.

В командной строке в каталоге, где находится main.go, снова выполните запуск:

$ go run .

В другом окне командной строки отправьте запрос к работающему веб-сервису с помощью curl:

$ curl http://localhost:8080/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'

Команда должна вывести заголовки ответа и JSON с добавленным альбомом:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

Как и в предыдущем разделе, используйте curl, чтобы получить полный список альбомов и убедиться, что новый альбом был добавлен:

$ curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

Команда должна вывести список альбомов:

[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    },
    {
        "id": "4",
        "title": "The Modern Sound of Betty Carter",
        "artist": "Betty Carter",
        "price": 49.99
    }
]

В следующем разделе вы добавите код для обработки GET-запроса к конкретному элементу.

Написание обработчика для возврата конкретного элемента

Когда клиент делает запрос GET /albums/[id], нужно вернуть альбом, чей идентификатор совпадает с параметром id в пути.

Для этого потребуется:

  • реализовать логику поиска нужного альбома;

  • сопоставить путь запроса с этой логикой.

Напишем код

Под функцией postAlbums, добавленной ранее, вставьте следующий код для поиска конкретного альбома:

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Loop over the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

В этом коде:

  • используется Context.Param, чтобы получить параметр id из URL. Когда вы будете привязывать обработчик к пути, в маршруте появится плейсхолдер для параметра;

  • выполняется перебор среза albums в поисках альбома, у которого поле ID совпадает с переданным параметром. Если альбом найден, он сериализуется в JSON и возвращается клиенту со статусом 200 OK;

  • если альбом не найден, возвращается ошибка со статусом 404 Not Found и сообщением "album not found".

В реальном сервисе поиск, скорее всего, выполнялся бы запросом к базе данных.

Теперь измените функцию main, чтобы добавить новый маршрут:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

Здесь путь /albums/:id сопоставляется с функцией getAlbumByID. В Gin двоеточие перед элементом пути означает, что это параметр пути.

Запуск кода

Если сервер ещё работает, остановите его. Затем в каталоге с файлом main.go снова выполните:

$ go run .

В новом окне командной строки отправьте запрос:

$ curl http://localhost:8080/albums/2

Команда должна вывести JSON с альбомом по указанному ID:

{
    "id": "2",
    "title": "Jeru",
    "artist": "Gerry Mulligan",
    "price": 17.99
}

Если альбом не найден, вернётся JSON с сообщением об ошибке.

Русскоязычное Go сообщество

Друзья! Эту статью перевела команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!

Заключение

Поздравляем! Вы только что создали простой RESTful веб-сервис на Go и Gin.

Ниже приведён полный код приложения, который мы написали:
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Loop through the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

Комментарии (0)