Создание API книжного магазина на Golang с помощью Gin

Введение

Сообщество Go считает, что для разработки веб-сервисов нам не нужен фреймворк. И я с этим согласен. Когда вы работаете без использования каких-либо фреймворков, вы узнаете все тонкости разработки. Но как только вы узнаете, как все работает, вы должны жить в сообществе и не изобретать велосипед.

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

Хотя я пишу этот пост как отдельную статью и хотел бы упростить ситуацию. Но я хочу продолжить развивать эти примеры, чтобы охватить аутентификацию, авторизацию, базы данных (включая Postgres, ORM), swagger и GraphQL. Я буду связывать эти посты, когда буду их создавать.

Почему Gin

Существует множество причин, по которым вы можете захотеть использовать Gin. Если вы спросите меня, то я большой поклонник разумных настроек Gin по умолчанию.

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

Hello World в Gin

package main

import (
    "net/http"

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

func main() {
    router := gin.New()

    router.GET("/ping", func(ctx *gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

    router.Run()
}

Вход в полноэкранный режим Выход из полноэкранного режима

Давайте немного познакомимся с Gin.

router := gin.New()
Вход в полноэкранный режим Выход из полноэкранного режима

Это создает экземпляр Engine. gin.Engine — это ядро gin. Он также действует как маршрутизатор, поэтому мы поместили экземпляр Engine в переменную router.

router.GET("/ping", func(ctx *gin.Context) {
Вход в полноэкранный режим Выйти из полноэкранного режима

Это связывает маршрут /ping с обработчиком. В приведенном примере я создал анонимную функцию, но это может быть и отдельная функция. Главное, на что следует обратить внимание, это параметр этой функции. *gin.Context. Context — это еще одна важная конструкция, помимо Engine. Context имеет почти 100 методов, привязанных к нему. Новичок должен потратить большую часть своего времени на понимание этой структуры Context и ее методов.

Давайте рассмотрим следующие несколько строк:

        ctx.JSON(http.StatusFound, gin.H{
            "message": "pong",
        })
Вход в полноэкранный режим Выход из полноэкранного режима

Одним из методов *gin.Context является JSON. Этот метод используется для отправки клиенту ответа в формате JSON. Это означает, что он автоматически устанавливает Content-Type ответа на application/json. Метод JSON принимает код состояния HTTP и карту ответа. gin.H является псевдонимом для map[string]interface{}. Таким образом, мы можем создать объект, который может иметь строковый ключ и любое значение, которое мы хотим.

Далее:

router.Run()
Вход в полноэкранный режим Выход из полноэкранного режима

Engine.Run просто берет наш маршрутизатор вместе с обработчиком маршрутов и привязывает его к http.Server. По умолчанию используется порт 8080, но если вы хотите, вы можете передать сюда другой адрес.

API книжного магазина

Ранее я уже делал POC на книжном магазине, тогда я хотел создать прототип соединения между MongoDB и Go. Но в этот раз моя цель — подключить Postgres и GraphQL.

Итак, прежде всего, я хотел бы, чтобы вы создали структуру каталогов следующим образом:

$ tree
.
├── db
│   └── db.go
├── go.mod
├── go.sum
├── handlers
│   └── books.go
├── main.go
└── models
    └── book.go
Войти в полноэкранный режим Выход из полноэкранного режима

И давайте начнем заполнять эти файлы.

db/db.go

package db

import "github.com/santosh/gingo/models"

// Books slice to seed book data.
var Books = []models.Book{
    {ISBN: "9781612680194", Title: "Rich Dad Poor Dad", Author: "Robert Kiyosaki"},
    {ISBN: "9781781257654", Title: "The Daily Stotic", Author: "Ryan Holiday"},
    {ISBN: "9780593419052", Title: "A Mind for Numbers", Author: "Barbara Oklay"},
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Вместо того чтобы вдаваться в сложности настройки базы данных прямо сейчас, я решил использовать для этого поста базу данных in-memory. В этом файле я засеял фрагмент db.Books некоторыми книгами.

Если models.Book сделает, вам интересно, то следующий файл будет только для этого.

models/book.go

package models

// Book represents data about a book.
type Book struct {
    ISBN   string  `json:"isbn"`
    Title  string  `json:"title"`
    Author string  `json:"author"`
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь нет ничего причудливого, у нас только 3 поля на данный момент. Все они строковые и с тегами struct.

Давайте посмотрим на наш main.go, прежде чем переходить к handlers.go.

main.go

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/santosh/gingo/handlers"
)

func setupRouter() *gin.Engine {
    router := gin.Default()
    router.GET("/books", handlers.GetBooks)
    router.GET("/books/:isbn", handlers.GetBookByISBN)
    // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
    // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
    router.POST("/books", handlers.PostBook)

    return router
}

func main() {
    router := setupRouter()
    router.Run(":8080")
}
Вход в полноэкранный режим Выход из полноэкранного режима

Почти аналогично примеру hello world, который мы видели выше. Но на этот раз у нас gin.Default() вместо gin.New(). Default поставляется с настройками по умолчанию, которые большинство из нас хотели бы иметь. Например, промежуточное ПО для ведения логов.

Честно говоря, я еще не использовал много промежуточного ПО Gin. Но создавать свои промежуточные программы чертовски просто. Я помещу несколько ссылок внизу статьи для вашего изучения. А пока давайте посмотрим на наши обработчики.

handlers/books.go

package handlers

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/santosh/gingo/db"
    "github.com/santosh/gingo/models"
)

// GetBooks responds with the list of all books as JSON.
func GetBooks(c *gin.Context) {
    c.JSON(http.StatusOK, db.Books)
}

// PostBook takes a book JSON and store in DB.
func PostBook(c *gin.Context) {
    var newBook models.Book

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

    // Add the new book to the slice.
    db.Books = append(db.Books, newBook)
    c.JSON(http.StatusCreated, newBook)
}

// GetBookByISBN locates the book whose ISBN value matches the isbn
func GetBookByISBN(c *gin.Context) {
    isbn := c.Param("isbn")

    // Loop over the list of books, look for
    // an book whose ISBN value matches the parameter.
    for _, a := range db.Books {
        if a.ISBN == isbn {
            c.JSON(http.StatusOK, a)
            return
        }
    }
    c.JSON(http.StatusNotFound, gin.H{"message": "book not found"})
}

// func DeleteBookByISBN(c *gin.Context) {}

// func UpdateBookByISBN(c *gin.Context) {}
Вход в полноэкранный режим Выход из полноэкранного режима

Настоящий сок находится в этом файле обработчиков. Это может потребовать некоторых пояснений.

handlers.GetBooks, который связан с GET /books, передает весь фрагмент книги.

handlers.GetBookByISBN, который связан с GET /books/:isbn, делает то же самое, но он также принимает isbn в качестве параметра URL. Этот обработчик сканирует весь фрагмент и возвращает найденную книгу. Сканирование большого фрагмента было бы не самым оптимальным решением, но не забывайте, что мы будем реализовывать настоящую базу данных, продолжая развивать этот книжный магазин.

Наиболее интересным здесь является handlers.PostBook, который привязан к POST /books. c.BindJSON — это волшебный метод, который принимает JSON из запроса и сохраняет его в ранее созданную структуру newBook. В дальнейшем

Тесты

На данный момент нам нужно небольшое изменение. Нам нужно удалить это содержимое из main.go:

@@ -1,17 +1,9 @@
 package main

-import (
-       "github.com/gin-gonic/gin"
-       "github.com/santosh/gingo/handlers"
-)
+import "github.com/santosh/gingo/routes"

 func main() {
-       router := gin.Default()
-       router.GET("/books", handlers.GetBooks)
-       router.GET("/books/:isbn", handlers.GetBookByISBN)
-       // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
-       // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
-       router.POST("/books", handlers.PostBook)
+       router := routes.SetupRouter()

        router.Run(":8080")
 }
Войти в полноэкранный режим Выход из полноэкранного режима

И поместить его в новый файл.

routes/roures.go

package routes

import (
    "github.com/gin-gonic/gin"
    "github.com/santosh/gingo/handlers"
)

func SetupRouter() *gin.Engine {
    router := gin.Default()
    router.GET("/books", handlers.GetBooks)
    router.GET("/books/:isbn", handlers.GetBookByISBN)
    // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
    // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
    router.POST("/books", handlers.PostBook)

    return router
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь есть изменения, которые имеют смысл для вас. Мы сделали это потому, что нам нужно запустить сервер из наших тестов.

Далее мы создаем books_test.go в handlers.

handlers/books_test.go

package handlers_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/santosh/gingo/models"
    "github.com/santosh/gingo/routes"
    "github.com/stretchr/testify/assert"
)


func TestBooksRoute(t *testing.T) {
    router := routes.SetupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/books", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "9781612680194")
    assert.Contains(t, w.Body.String(), "9781781257654")
    assert.Contains(t, w.Body.String(), "9780593419052")
}

func TestBooksbyISBNRoute(t *testing.T) {
    router := routes.SetupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/books/9781612680194", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "Rich Dad Poor Dad")
}


func TestPostBookRoute(t *testing.T) {
    router := routes.SetupRouter()

    book := models.Book{
        ISBN: "1234567891012",
        Author: "Santosh Kumar",
        Title: "Hello World",
    }

    body, _ := json.Marshal(book)

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/books", bytes.NewReader(body))
    router.ServeHTTP(w, req)

    assert.Equal(t, 201, w.Code)
    assert.Contains(t, w.Body.String(), "Hello World")
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Также, опять же, практически все понятно. Я не думаю, что приведенный выше код нуждается в пояснениях. Мы тестируем коды ответов и тела ответов для определенной строки.

Давайте также запустим тесты и проверим, как все прошло:

$ go test ./... -cover
?       github.com/santosh/gingo        [no test files]
?       github.com/santosh/gingo/db     [no test files]
ok      github.com/santosh/gingo/handlers       (cached)        coverage: 83.3% of statements
?       github.com/santosh/gingo/models [no test files]
?       github.com/santosh/gingo/routes [no test files]
Вход в полноэкранный режим Выход из полноэкранного режима

Упражнение

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

  1. Реализуйте обработчики DeleteBookByISBN и UpdateBookByISBN и включите их.
  2. Напишите тесты для вышеупомянутых обработчиков.
  3. Наши тесты очень простые. Как и наши обработчики. Мы не делаем никакой обработки ошибок. Добавьте обработку ошибок в обработчики и напишите тесты для их проверки.

Заключение

Мы увидели, как просто создать приложение hello world в Gin. Но на этом наше путешествие не заканчивается. В следующий раз я вернусь с новыми уроками. А пока до свидания.

Связанные ссылки

  • https://sosedoff.com/2014/12/21/gin-middleware.html
  • https://santoshk.dev/posts/2020/sending-post-request-in-go-with-a-body/

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *