Команда 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"})
}
 
          