Введение
Сегодня мы покажем, как создавать веб-приложения и микросервисы в Go с помощью фреймворка Gin. Gin это фреймворк, позволяющий уменьшить объём кода, необходимого для построения таких приложений. Он поощряет создание многократно-используемого и расширяемого кода.
Мы рассмотрим создание проекта и сборку несложного приложения с Gin, которое будет выводить список топиков и отдельный топик.
Подготовка
Перед началом работы убедитесь, что у вас установлены Go и утилита curl. Если curl не установлена и вы не хотите работать с ней, используйте любую другую утилиту тестирования API.
Что такое Gin?
Gin это высокопроизводительный микрофреймворк, который используется для создания веб-приложений и микросервисов. С ним очень удобно делать комплексную конвейерную обработку запросов из модулей — многократно используемых кусочков кода. Вы пишете промежуточный слой приложения, который затем подключается в один или более обработчик запросов или в группу обработчиков.
Почему именно Gin?
Одно из лучших качеств Go — его встроенная библиотека net/http, позволяющая с лёгкостью создавать HTTP сервер. Однако, она не настолько гибкая, как бы хотелось, и количество кода, требуемое при работе с ней, довольно большое.
В Go нет встроенной поддержки обработчика роутов на базе регулярных выражений. Вам нужно писать код для получения этого функционала. Однако, с ростом количества ваших приложений, вы будете вынуждены копировать один и тот же код везде или всё-таки создадите библиотеку.
В этом и есть задача Gin. Он содержит набор часто употребляемых функций, таких как роутинг, поддержка middleware, обработка шаблонов. Вдобавок к этому, он позволяет уменьшить количество кода в приложениях и создание веб-приложений с ним намного проще.
Проектирование приложения
Посмотрим, как Gin обрабатывает запросы:
Request -> Route Parser -> [Optional Middleware] -> Route Handler -> [Optional Middleware] -> Response
Когда приходит запрос, Gin сначала проверяет, есть ли подходящий роут (маршрут). Если соответствующий роут найден, Gin запускает обработчик этого роута и промежуточные звенья в заданном порядке. Мы увидим как это происходит, когда перейдём к коду в следующем разделе.
Функционал приложения
Наше приложение — это простой менеджер топиков. Оно должно:
- позволять пользователям регистрироваться с логином и паролем (для неавторизованных пользователей),
- позволять пользователям авторизоваться (для неавторизованных пользователей),
- позволять пользователям завершать сеанс (для авторизованных пользователей),
- позволять пользователям создавать топики (для авторизованных пользователей),
- Выводить список всех топиков на главной странице (для всех пользователей), и
- Выводить топик на его собственной странице (для всех пользователей). Вдобавок к этому мы сделаем, чтобы список топиков и отдельные топики были доступны в форматах HTML, JSON и XML.
Это позволит нам проиллюстрировать, как можно использовать Gin для проектирования веб-приложений, API серверов и микросервисов.
Для этого мы используем следующий функционал Gin:
- Routing — для обработки различных URL адресов,
- Custom rendering — для обработки формата ответа, и
- Middleware — для реализации авторизации. Также мы напишем тесты для проверки работоспособности нашего функционала.
Routing
Роутинг (маршрутизация) это одна из важнейших функций, имеющихся во всех современных веб-фреймворках. Любая веб-страница или вызов API доступен по URL. Фреймворки используют роуты для обработки запросов к этим URL-адресам. Если URL такой: httр://www.example.com/some/random/route, то роут будет: /some/random/route.
У Gin очень быстрый роутер, удобный в конфигурировании и работе. Вместе с обработкой определенных URL-адресов, роутер в Gin может обрабатывать шаблоны адресов и группы URL.
В нашем приложении мы будем:
- Хранить главную страницу в роуте / (запрос HTTP GET),
- Группировать роуты, относящиеся к пользователям, в роуте /u ,
- Хранить страницу авторизации в /u/login (запрос HTTP GET),
- Передавать данные авторизации в /u/login (запрос HTTP POST),
- Завершение сеанса в /u/logout (запрос HTTP GET),
- Хранить страницу регистрации в /u/register (запрос HTTP GET),
- Передавать регистрационную информацию в /u/register (запрос HTTP POST) ,
- Группировать роуты, относящиеся к топикам, в роуте /article,
- Хранить страницу создания топика в /article/create (запрос HTTP GET),
- Передавать утверждённый топик в /article/create (запрос HTTP POST), и
- Хранить страницу топика в /article/view/:article_id (запрос HTTP GET). Обратите внимание на часть :article_id в этом роуте. Двоеточие : в начале указывает на то, что это динамический роут. Это значит, что :article_id может содержать любое значение и Gin сделает это значение доступным в обработчике запроса.
Rendering
Веб-приложение может вывести ответ в различных форматах, таких как HTML, текст, JSON, XML или другие форматы. API и микросервисы обычно отдают данные в формате JSON, но здесь также нет ограничений.
В следующем разделе мы увидим, как можно обработать разные типы ответов без дублирования функционала. По-умолчанию мы будем отвечать на запрос шаблоном HTML. Однако, мы создадим ещё два вида запроса, которые будут отвечать в формате JSON или XML.
Middleware
В контексте веб-приложений на Go, middleware это часть кода, которую можно выполнить на любом этапе обработки HTTP-запроса. Обычно их используют для инкапсуляции типового функционала, который вам нужно вызывать из различных роутов. Мы можем использовать middleware перед и/или после обработанного HTTP-запроса. К типовым примерам применения middleware относятся авторизация, валидация и т.п.
Если middleware используется перед обработкой роута, любые изменения, сделанные им, будут доступны в главном обработчике запросов. Это удобно, если мы хотим реализовать проверку определённых запросов. С другой стороны, если middleware используется после обработчика, он получит ответ из обработчика роутов. Это можно использовать для модификации ответа из обработчика роута.
Мы должны быть уверены, что некоторые страницы и действия, к примеру, создание топика, завершение сеанса, доступны только авторизованным пользователям. И также необходимо, чтобы некоторые страницы и действия, к примеру, регистрация, авторизация, были доступны только неавторизованным пользователям.
Если мы включим соответствующую логику в каждый роут, это будет сложно, излишне повторяемо и склонно к ошибкам. К счастью, мы можем создать middleware для каждой из этих задач и многократно использовать их в соответствующих роутах.
Мы создадим middleware, которое будет применимо ко всем роутам. Наше middleware (setUserStatus) будет проверять — от авторизованного пользователя пришёл запрос или от неавторизованного. Затем оно установит флаг, который можно будет использовать в шаблонах для настройки видимости определённых ссылок в меню приложения.
Установка зависимостей
Наше приложение будет использовать только одну внешнюю зависимость — сам фреймворк Gin. Установим актуальную версию такой командой:
go get -u github.com/gin-gonic/gin
Создание многократно-используемых шаблонов
Наше приложение будет отображать веб-страницу, используя её шаблон. Однако, в ней будет несколько частей, таких как шапка (header), меню, боковая панель и подвал (footer), которые будут представлены на всех страницах. В Go можно создавать шаблонные снипеты, которые можно будет загружать в любые шаблоны.
Мы создадим снипеты для шапки и подвала, также создадим меню в соответствующем файле-шаблоне, которое затем вызовем из шапки. Ну и наконец, мы создадим шаблон главной страницы, с которой вызовем шапку и подвал. Все файлы шаблонов будут размещаться в папке templates нашего проекта.
Сначала создайте шаблон меню в файле templates/menu.html как описано ниже:
<!--menu.html-->
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">
Home
</a>
</div>
</div>
</nav>
Пока в нашем меню есть только одна ссылка на главную страницу. Позже мы добавим остальные ссылки по мере роста функционала приложения. Шаблон шапки будет в файле templates/header.html:
<!--header.html-->
<!doctype html>
<html>
<head>
<!--Use the `title` variable to set the title of the page-->
<title>{{ .title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<!--Use bootstrap to make the application look nice-->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script async src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body class="container">
<!--Embed the menu.html template at this location-->
{{ template "menu.html" . }}
Как вы видите, мы используем здесь фреймворк с открытым исходным кодом Bootstrap. Большая часть файла это стандартный HTML. Однако, посмотрим внимательно на пару строк. В строке с {{ .title }} динамически задаётся заголовок страницы с помощью переменной .title, которая должна быть определена в приложении. А в строке {{ template «menu.html». }} мы загружаем шаблон меню из файла menu.html. Вот так в Go можно вызывать один шаблон из другого.
Шаблон подвала содержит только статический HTML. Шаблон главной страницы вызывает шапку и подвал и выводит сообщение Hello Gin:
<!--index.html-->
<!--Embed the header.html template at this location-->
{{ template "header.html" .}}
<h1>Hello Gin!</h1>
<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}
По аналогии с шаблоном главной, в шаблонах других страниц мы также используем эти шапку и подвал.
Завершение и проверка установки
Создав шаблоны, теперь самое время создать главный файл приложения. Мы создадим файл main.go, в нём будет простое веб-приложение, загружающее главную страницу. С Gin это делается в четыре шага:
1. Создаём роутер
Роутер в Gin создаётся так:
router := gin.Default()
2. Загружаем шаблоны
После создания роутера, загрузим все шаблоны:
router.LoadHTMLGlob("templates/*")
Это загрузит все шаблоны из папки templates. Загрузив один раз шаблоны, больше не будет необходимости перечитывать их, что делает веб-приложения с Gin очень быстрыми.
3. Задаём обработчик роутов
Очень важно правильно спроектировать приложение, разделив на соответствующие роуты и задав обработчики для каждого из них. Мы создадим роут для главной страницы и его обработчик.
router.GET("/", func(c *gin.Context) {
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses (in this case, 'title')
gin.H{
"title": "Home Page",
},
)
})
С помощью метода router.GET мы задаём обработчик роута для GET-запросов. Он принимает в качестве параметров сам роут (/) и один или несколько обработчиков, которые всего лишь функции.
Обработчик роута имеет указатель на Контекст (gin.Context) в параметрах. В этом контексте содержится вся информация о запросе, которая может понадобится обработчику в дальнейшем. К примеру, в нём есть информация о заголовках, cookies и т.д.
В Контексте также есть методы для вывода ответа в форматах HTML, тексте, JSON и XML. В нашем случае мы взяли метод context.HTML для обработки HTML шаблона (index.html). Вызов этого метода включает дополнительные данные, в которых значение title установлено Home Page. Это значение, которое может быть обработано в HTML шаблоне. Мы используем это значение в теге в шаблоне шапки.
4. Запуск приложения
Для запуска приложения воспользуемся методом Run нашего роутера:
router.Run()
Приложение запустится на localhost и 8080 порте, по-умолчанию.
Финальный файл main.go будет таким:
// main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
var router *gin.Engine
func main() {
// Set the router as the default one provided by Gin
router = gin.Default()
// Process the templates at the start so that they don't have to be loaded
// from the disk again. This makes serving HTML pages very fast.
router.LoadHTMLGlob("templates/*")
// Define the route for the index page and display the index.html template
// To start with, we'll use an inline route handler. Later on, we'll create
// standalone functions that will be used as route handlers.
router.GET("/", func(c *gin.Context) {
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses (in this case, 'title')
gin.H{
"title": "Home Page",
},
)
})
// Start serving the application
router.Run()
}
Для запуска приложения из командной строки, перейдите в папку приложения и выполните команду:
go build -o app
Будет собрано приложение и создан исполняемый файл с именем app, который можно запустить так:
./app
Если всё прошло успешно, вы должны увидеть приложение по адресу http://localhost:8080 и оно будет выглядеть примерно так:
На этом этапе иерархия папок приложения будет такой:
├── main.go
└── templates
├── footer.html
├── header.html
├── index.html
└── menu.html
Выводим список топиков
Сейчас мы добавим функционал для показа списка всех топиков на главной странице.
Настройка роута
В предыдущем разделе мы создали роут и его описание в файле main.go. С ростом размера приложения будет лучше перенести описания роутов в отдельный файл. Мы создадим функцию initializeRoutes() в файле routes.go и будем вызывать её из функции main() для настройки всех роутов. Вместо создания линейного обработчика роутов, мы сделаем роуты отдельными функциями.
После всего этого файл routes.go будет таким:
// routes.go
package main
func initializeRoutes() {
// определение роута главной страницы
router.GET("/", showIndexPage)
}
Так как мы выводим список топиков на главной странице, нам не нужно будет создавать больше никаких других роутов.
Файл main.go должен быть сейчас таким:
// main.go
package main
import "github.com/gin-gonic/gin"
var router *gin.Engine
func main() {
// роутер по-умолчанию в Gin
router = gin.Default()
// Обработаем шаблоны вначале, так что их не нужно будет перечитывать
// ещё раз. Из-за этого вывод HTML-страниц такой быстрый.
router.LoadHTMLGlob("templates/*")
// Инициализируем роуты
initializeRoutes()
// Запускаем приложение
router.Run()
}
Проектирование модели топика
Сделаем структуру топика простой, всего с тремя полями — Id, Title (название) и Content (содержание). Её можно описать так:
type article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
Большинство приложений используют базу данных для хранения данных. Чтобы не усложнять, мы будем хранить список топиков в памяти и заполнять его при создании двумя следующими топиками:
var articleList = []article{
article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}
Мы вставим этот кусок кода в новый файл models.article.go. Сейчас нам понадобится функция, возвращающая список всех топиков. Мы её назовём getAllArticles() и положим её в этот же файл. И создадим тест для неё. Мы назовём этот тест TestGetAllArticles и вставим его в файл models.article_test.go.
Создадим тест (TestGetAllArticles) для функции getAllArticles(). В результате файл models.article_test.go будет таким:
// models.article_test.go
package main
import "testing"
// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
alist := getAllArticles()
// Check that the length of the list of articles returned is the
// same as the length of the global variable holding the list
if len(alist) != len(articleList) {
t.Fail()
}
// Check that each member is identical
for i, v := range alist {
if v.Content != articleList[i].Content ||
v.ID != articleList[i].ID ||
v.Title != articleList[i].Title {
t.Fail()
break
}
}
}
В этом тесте используется функция getAllArticles() для получения списка всех топиков. Сперва этот тест проверяет, что эта функция получает список топиков и этот список идентичен списку, загруженному в глобальную переменную articleList. Затем он проходит в цикле по списку топиков для проверки уникальности каждого. Если хотя бы одна из этих проверок не удалась, тест возвращает неудачу.
После написания теста приступим к написанию кода модуля. Файл models.article.go будет содержать такой код:
// models.article.go
package main
type article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}
// Return a list of all the articles
func getAllArticles() []article {
return articleList
}
Создание шаблона представления
Так как список топиков будет выводится на главной странице, нам не нужно создавать новый шаблон. Однако, нам нужно изменить шаблон index.html для вывода в него списка топиков.
Предположим, что список топиков будет передан в шаблон в переменной payload. Тогда следующий снипет выведет список всех топиков:
{{range .payload }}
<!--Create the link for the article based on its ID-->
<a href="/article/view/{{.ID}}">
<!--Display the title of the article -->
<h2>{{.Title}}</h2>
</a>
<!--Display the content of the article-->
<p>{{.Content}}</p>
{{end}}
Этот снипет пройдётся по всем элементам переменной payload и выведет название и текст каждого топика. Также этот снипет добавит ссылку в каждый топик. Однако, пока мы ещё не создали обработчик соответствующего роута, и эти ссылки на топики не будут работать.
Обновлённый index.html будет таким:
<!--index.html-->
<!--Embed the header.html template at this location-->
{{ template "header.html" .}}
<!--Loop over the `payload` variable, which is the list of articles-->
{{range .payload }}
<!--Create the link for the article based on its ID-->
<a href="/article/view/{{.ID}}">
<!--Display the title of the article -->
<h2>{{.Title}}</h2>
</a>
<!--Display the content of the article-->
<p>{{.Content}}</p>
{{end}}
<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}
Определяем требования к обработчику роута с помощью юнит-теста
Перед созданием обработчика роута главной страницы, мы создадим тест, чтобы определить поведение этого обработчика. Этот тест проверит следующие условия:
- Обработчик отвечает статус-кодом HTTP 200,
- Возвращаемый HTML содержит тег title с текстом «Home Page». Код теста поместим в функцию TestShowIndexPageUnauthenticated в файл handlers.article_test.go. Вспомогательные функции, используемые в этом тесте, мы разместим в файле common_test.go.
Вот содержимое файла handlers.article_test.go:
// handlers.article_test.go
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
r := getRouter(true)
r.GET("/", showIndexPage)
// Create a request to send to the above route
req, _ := http.NewRequest("GET", "/", nil)
testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
// Test that the http status code is 200
statusOK := w.Code == http.StatusOK
// Test that the page title is "Home Page"
// You can carry out a lot more detailed tests using libraries that can
// parse and process HTML pages
p, err := ioutil.ReadAll(w.Body)
pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0
return statusOK && pageOK
})
}
Файл common_test.go:
package main
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/gin-gonic/gin"
)
var tmpArticleList []article
// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
//Set Gin to Test Mode
gin.SetMode(gin.TestMode)
// Run the other tests
os.Exit(m.Run())
}
// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
r := gin.Default()
if withTemplates {
r.LoadHTMLGlob("templates/*")
}
return r
}
// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {
// Create a response recorder
w := httptest.NewRecorder()
// Create the service and process the above request.
r.ServeHTTP(w, req)
if !f(w) {
t.Fail()
}
}
// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
tmpArticleList = articleList
}
// This function is used to restore the main lists from the temporary one
func restoreLists() {
articleList = tmpArticleList
}
Для написания теста мы написали несколько вспомогательных функций. Они в дальнейшем помогут нам уменьшить количество кода при написании похожих тестов.
Функция TestMain переводит Gin в тестовый режим и вызывает функции тестирования. Функция getRouter создаёт и возвращает роутер. Функция saveLists() помещает список топиков во временную переменную. Она используется в функции restoreLists() для восстановления списка топиков до первоначального состояния после выполнения юнит-теста.
И, наконец, функция testHTTPResponse выполняет переданную ей функцию для проверки — возвращает ли она булево значение true — показывая успешность теста, или нет. Эта функция помогает нам избежать дублирования кода, необходимого для тестирования ответа на HTTP-запрос.
Для проверки HTTP-кода и возвращаемого HTML, сделаем следующее:
- Создадим новый роутер,
- Определим роуту тот же обработчик, что используется в главном приложении (showIndexPage),
- Создадим новый запрос для доступа к роуту,
- Создадим функцию, обрабатывающую ответ, для тестирования HTTP-кода и HTML, и
- Вызовем testHTTPResponse() из новой функции для завершения теста.
Создание обработчика роута
Мы будет создавать все обработчики роутов, относящихся к топикам, в файле handlers.article.go. Обработчик главной страницы, showIndexPage выполняет следующие задачи:
1. Получает список топиков
Это делается с помощью функции getAllArticles, созданной ранее:
articles := getAllArticles()
2. Обрабатывает шаблон index.html, передавая ему список топиков
Это делается с помощью кода ниже:
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
Разница с кодом из предыдущего раздела только в том, что мы передаём список топиков, который можно прочитать в шаблоне в переменной payload.
Файл handlers.article.go должен быть таким:
// handlers.article.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
Если сейчас собрать и запустить приложение, открыть в браузере http://localhost:8080, оно будет выглядеть так:
Новые файлы, добавленные в этом разделе:
├── common_test.go
├── handlers.article.go
├── handlers.article_test.go
├── models.article.go
├── models.article_test.go
└── routes.go
Вывод топика
У нас пока не работают ссылки на топики из общего списка. Сейчас мы добавим обработчики клика и шаблон для вывода топика.
Настройка роутов
Мы можем создать роут для обработки запросов для топика подобно роуту из предыдущей части. Однако, мы должны учитывать, что хотя обработчик для всех топиков будет один, URL каждого топика должен быть уникальным. Gin позволяет это сделать с помощью передачи параметров в роут:
router.GET("/article/view/:article_id", getArticle)
Этот роут будет обрабатывать соответствующие указанному пути запросы, а также хранить значение параметра, переданного в роут — article_id, который мы обработаем в обработчике роута. Обработчиком нашего роута будет функция getArticle.
Изменённый файл routes.go:
// routes.go
package main
func initializeRoutes() {
// обработчик главного роута
router.GET("/", showIndexPage)
// Обработчик GET-запросов на /article/view/некоторый_article_id
router.GET("/article/view/:article_id", getArticle)
}
Шаблоны
Для вывода топика нам нужно создать новый шаблон templates/article.html. Он будет создан так же, как шаблон index.html, но с небольшим отличием: вместо передачи в него переменной со списком топиков, мы будем передавать в него только один топик.
Посмотреть код шаблона article.html можно на Github.
Определяем требования к обработчику роутов юнит-тестами\
Тест обработчика будет проверять выполнение следующих условий:
- Обработчик отвечает статус-кодом HTTP 200,
- Возвращаемый HTML содержит тег title, содержащий название полученного топика. Код теста будет в функции TestArticleUnauthenticated в файле handlers.article_test.go. Вспомогательные функции мы разместим в файле common_test.go.
Создаём обработчик роута
Итак, что должен делать обработчик роута для топика — getArticle:
1. Получить ID топика для вывода
Для вывода нужного топика, мы должны получить его ID из контекста. Примерно так:
c.Param("article_id")
где c — это Контекст Gin, который передаётся параметром в любой обработчик при разработке с Gin.
2. Получить сам топик
Это можно сделать с помощью функции getArticleByID() из файла models.article.go:
article, err := getArticleByID(articleID)
Функция getArticleByID (в models.article.go) выглядит так:
func getArticleByID(id int) (*article, error) {
for _, a := range articleList {
if a.ID == id {
return &a, nil
}
}
return nil, errors.New("Article not found")
}
Эта функция считывает список топиков в цикле и возвращает топик, ID которого соответствует переданному ID. Если такого топика нет, она возвращает ошибку.
3. Обработать шаблон article.html, передав в него топик
Код ниже как раз делает это:
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон article.html
"article.html",
// Передадим данные в шаблон
gin.H{
"title": article.Title,
"payload": article,
},
)
Обновлённый файл handlers.article.go будет таким:
// handlers.article.go
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Вызовем метод HTML из Контекста Gin для обработки шаблона
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон index.html
"index.html",
// Передадим данные в шаблон
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
func getArticle(c *gin.Context) {
// Проверим валидность ID
if articleID, err := strconv.Atoi(c.Param("article_id")); err == nil {
// Проверим существование топика
if article, err := getArticleByID(articleID); err == nil {
// Вызовем метод HTML из Контекста Gin для обработки шаблона
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон index.html
"article.html",
// Передадим данные в шаблон
gin.H{
"title": article.Title,
"payload": article,
},
)
} else {
// Если топика нет, прервём с ошибкой
c.AbortWithError(http.StatusNotFound, err)
}
} else {
// При некорректном ID в URL, прервём с ошибкой
c.AbortWithStatus(http.StatusNotFound)
}
}
Если сейчас собрать и запустить наше приложение, при открытии localhost:8080/article/view/1 в браузере оно будет выглядеть так:
Новые файлы, добавленные в этом разделе:
└── templates
└── article.html
Ответ в JSON/XML
В этом разделе мы немного перепишем приложение так, что оно, в зависимости от заголовков запроса, будет отвечать в формате HTML, JSON или XML.
Повторно используемые функции
До сих пор мы использовали метод HTML Контекста Gin для обработки шаблонов прямо из обработчика. Этот способ хорошо если мы всегда будем выводить только в формате HTML. Однако, если мы хотим менять формат ответа, к примеру, на основе какого-то параметра, мы должны переписать эту часть функции, чтобы она делала только валидацию данных и их получение, а выводом в шаблон будет заниматься другая функция в зависимости от формата вывода на основе заголовка Accept. Мы создадим эту функция в файле main.go и она будет общая для всех обработчиков.
В Gin в Контексте, переданном обработчику роута, есть поле Request. В этом поле есть Header, в котором содержатся все заголовки запроса. Для получения заголовка Accept мы можем использовать метод Get в Header, вот так:
// c - это Gin Context
c.Request.Header.Get("Accept")
- Если заголовок: application/json, функция выводит JSON,
- Если заголовок: application/xml, функция выводит XML, и
- Если заголовок любой другой или вообще пустой, функция выводит HTML. Полный код функции:
// Render one of HTML, JSON or CSV based on the 'Accept' header of the request
// If the header doesn't specify this, HTML is rendered, provided that
// the template name is present
func render(c *gin.Context, data gin.H, templateName string) {
switch c.Request.Header.Get("Accept") {
case "application/json":
// Respond with JSON
c.JSON(http.StatusOK, data["payload"])
case "application/xml":
// Respond with XML
c.XML(http.StatusOK, data["payload"])
default:
// Respond with HTML
c.HTML(http.StatusOK, templateName, data)
}
}
Изменяем требования к обработчику роутов
Так как мы теперь должны проверить ответ в JSON и XML если задан специальный заголовок, нам нужно добавить тесты в файл handlers.article_test.go для проверки этих условий:
- Проверить, что приложение вернёт список топиков в формате JSON если заголовок Accept равен application/json
- Проверить, что приложение вернёт список топиков в формате XML если заголовок Accept равен application/xml Мы добавим соответствующие функции TestArticleListJSON и TestArticleXML.
Обновляем обработчики
Обработчик у нас уже полностью готов, нам нужно только изменить метод обработки c.HTML на соответствующий требуемому формату метод вывода.
К примеру, обработчик роута showIndexPage будет изменён с такого:
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
на такой:
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the render function with the name of the template to render
render(c, gin.H{
"title": "Home Page",
"payload": articles}, "index.html")
}
Получаем список топиков в формате JSON
Чтобы увидеть приложение в работе, соберём его и запустим. Затем выполним следующую команду:
curl -X GET -H "Accept: application/json" http://localhost:8080/
Она должна вернуть следующее:
[{"id":1,"title":"Article 1","content":"Article 1 body"},{"id":2,"title":"Article 2","content":"Article 2 body"}]
Как вы видите, мы получили ответ в формате JSON, передав заголовок Accept как application/json.
Список топиков в формате XML
Теперь запросим детали конкретной статьи в формате XML. Для этого запустите приложение как написано выше и затем выполните команду:
curl -X GET -H "Accept: application/xml" http://localhost:8080/article/view/1
В ответ должно прийти следующее:
<article><ID>1</ID><Title>Article 1</Title><Content>Article 1 body</Content></article>
Тестирование приложения
Мы использовали тесты для определения требований к обработчикам роутов и моделям, поэтому можем теперь запустить их и проверить, что всё работает как предполагалось. В директории проекта запустите следующую команду:
go test -v
Результат должен быть примерно таким:
=== RUN TestShowIndexPageUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 | 183.315µs | | GET /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN TestArticleUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 | 143.789µs | | GET /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN TestArticleListJSON
[GIN] 2016/06/14 - 19:07:26 | 200 | 51.087µs | | GET /
--- PASS: TestArticleListJSON (0.00s)
=== RUN TestArticleXML
[GIN] 2016/06/14 - 19:07:26 | 200 | 38.656µs | | GET /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
PASS
ok github.com/demo-apps/go-gin-app 0.084s
Как мы видим, эта команда запускает все написанные нами тесты и, в нашем случае, сообщает, что всё работает как положено. Если вы присмотритесь к выводу, то увидите, что Go также сделал и HTTP запросы для нас, проверив обработчики роутов.
Заключение
В этом цикле статей мы сделали приложение с помощью фреймворка Gin и постепенно добавили в него функционал. Мы написали тесты, чтобы наше приложение было надёжным, а также использовали методологию повторно используемого кода, чтобы сделать вывод в различные форматы без больших затрат времени.
Код приложения можно скачать в этом Github репозитории.
Gin очень прост для того, чтобы начать писать веб-приложения — вкупе со встроенной функциональностью Go, он легко позволяет строить высококачественные, хорошо покрытые тестами веб-приложения и микросервисы.
По материалам Building Go Web Applications and Microservices Using Gin