суббота, 11 сентября 2021 г.

Разработка RESTful API с помощью Go и Gin

В этом посте представлены основы написания API RESTful веб-сервиса с помощью Go и Gin Web Framework (Gin).

Вы получите максимальную отдачу от этого руководства, если у вас будут базовые знания Go и его инструментов. Если это ваше первое знакомство с Go, см. Начало работы с Go для быстрого ознакомления.

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

В этом руководстве вы создадите RESTful API сервер с двумя конечными точками. Ваш примерный проект будет репозиторием данных о винтажных джазовых записях.

Предпосылки

1. Установка Go 1.16 или новее. Инструкции по установке см. Установка Go.

2. Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.

3. Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

4. Инструмент curl. В Linux и Mac это уже должно быть установлено. В Windows он входит в состав Windows 10 . Для более ранних версий Windows может потребоваться его установка.

Проектирование конечных точек API

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

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

Вот конечные точки, которые вы создадите в этом руководстве.

/albums

  • GET - получить список всех альбомов в формате JSON.
  • POST - добавить новый альбом из данных запроса, отправленных в формате JSON.

/albums/:id

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

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

Создайте папку для вашего кода

Для начала создайте проект для кода, который вы напишете.

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

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

2. Используя командную строку, создайте каталог для вашего кода с именем web-service-gin.

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

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

Запустите команду go mod init, указав ей путь к модулю, в котором будет находиться ваш код.

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

Эта команда создает файл go.mod, в котором будут перечислены добавленные вами зависимости для отслеживания. Дополнительные сведения об именовании модуля с помощью пути к модулю см. в посте Управление зависимостями.

Далее вы создадите структуры данных для обработки данных.

Создайте данные

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

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

Напишите код

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

В main.go в верхней части файла вставьте следующее объявление пакета.

package main

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

Под объявлением пакета вставьте следующее объявление структуры album. Вы будете использовать ее для хранения данных альбома в памяти.

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

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

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

// Срез albums для заполнения данных об альбомах.
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 создает JSON из среза album структур, записывая JSON в ответ.

// getAlbums возвращает список всех альбомов в формате 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, вставьте приведенный ниже код, чтобы назначить функцию-обработчик пути к конечной точке.

Это устанавливает связь, в которой getAlbums обрабатывает запросы к пути конечной точки /albums.

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

    router.Run("localhost:8080")
}

В этом коде вы:

  • Инициализируете роутер Gin, используя Default.
  • Используете функцию GET, чтобы связать метод GET HTTP и путь /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 к вашей логике.

Напишите код

Добавьте код для добавления данных альбомов в список альбомов.

Где-нибудь после операторов импорта вставьте следующий код. (Конец файла - хорошее место для этого кода, но Go не обеспечивает соблюдение порядка, в котором вы объявляете функции.)

// postAlbums добавляет альбом из JSON, 
// полученного в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

В этом коде вы:

  • Используйте Context.BindJSON для привязки тела запроса к newAlbum.
  • Добавляете структуру album, инициализированную из JSON, в срез albums.
  • Добавляете в ответ код состояния 201 вместе с JSON, представляющим добавленный вами альбом.

Измените main функцию так, чтобы она включала функцию router.POST, как показано ниже.

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

    router.Run("localhost:8080")
}

В этом коде вы:

  • Связываете метод POST по пути /albums с функцией postAlbums.
    С помощью Gin вы можете связать обработчик с комбинацией HTTP-метода и пути. Таким образом, вы можете отдельно маршрутизировать запросы, отправленные по одному пути, в зависимости от метода, который использует клиент.

Запустите код

Если сервер все еще работает с последнего раздела, остановите его.

Из командной строки в каталоге, содержащем 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 /album/[id], вы хотите вернуть альбом, идентификатор которого совпадает с параметром пути id.

Для этого вы:

  • Добавите логику для получения запрошенного альбома.
  • Укажите путь к логике.

Напишите код

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

Эта функция getAlbumByID извлечет идентификатор из пути запроса, а затем найдет соответствующий альбом.

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    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-адреса. Когда вы сопоставляете этот обработчик с путем, вы включаете в путь заполнитель для параметра.
  • Проходите в цикле по структурам album в срезе, ища ту, значение поля ID которой совпадает со значением параметра id. Если он найден, вы сериализуете эту album структуру в JSON и возвращаете ее в качестве ответа с HTTP-кодом 200 OK.
    Как упоминалось выше, реальный сервис, скорее всего, будет использовать запрос к базе данных для выполнения этого поиска.
  • Вернуть ошибку HTTP 404 с http.StatusNotFound, если альбом не найден.

Наконец, измените свой main так, чтобы он включал новый вызов router.GET, где теперь путь - /albums/:id, как показано в следующем примере.

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, чтобы сделать запрос к работающему веб-сервису.

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

Команда должна отображать JSON для альбома, идентификатор которого вы использовали. Если альбом не найден, вы получите JSON с сообщением об ошибке.

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

Заключение

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

Итоговый код

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

package main

import (
    "net/http"

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

// album представляет данные об альбоме.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// Срез albums для заполнения данных об альбомах.
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 возвращает список всех альбомов в формате JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums добавляет альбом из JSON, полученный в теле запроса.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Вызов BindJSON для привязки 
    // полученного JSON к newAlbum
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Добавляем в срез новый альбом.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID находит альбом, 
// значение идентификатора которого совпадает с 
// параметром id, отправленным клиентом,
// затем возвращает этот альбом в качестве ответа
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Перебираем список альбомов в поисках альбома,
    // значение идентификатора которого соответствует параметру.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}


Читайте также:


Комментариев нет:

Отправить комментарий