Введение
Сообщество 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]
Упражнение
Да, давайте сделаем этот пост в блоге более интересным, добавив немного интерактивности. У меня есть для вас несколько задач, которые вы должны решить самостоятельно. Пожалуйста, попробуйте их решить. Вот они:
- Реализуйте обработчики
DeleteBookByISBN
иUpdateBookByISBN
и включите их. - Напишите тесты для вышеупомянутых обработчиков.
- Наши тесты очень простые. Как и наши обработчики. Мы не делаем никакой обработки ошибок. Добавьте обработку ошибок в обработчики и напишите тесты для их проверки.
Заключение
Мы увидели, как просто создать приложение 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/