Skip to content

Gin

Официальная документация: Gin Web Framework (gin-gonic.com)

Адрес репозитория: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

Официальные примеры: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

Введение

Gin — это HTTP веб-фреймворк, написанный на Go (Golang). Он имеет API, похожий на martini, но с гораздо лучшей производительностью, до 40 раз быстрее благодаря httprouter. Если вам нужны производительность и хорошая продуктивность, вам обязательно понравится Gin. Gin по сравнению с Iris и Beego является более лёгким фреймворком, отвечает только за веб-часть, стремится к максимальной производительности маршрутизации. Функциональность может быть не такой полной, но он лёгкий и расширяемый, что является его преимуществом. Поэтому среди всех веб-фреймворков Gin самый простой для освоения и изучения.

Особенности

  • Быстрый: Маршрутизация на основе дерева Radix, небольшой объём памяти. Нет рефлексии. Предсказуемая производительность API.
  • Поддержка промежуточного ПО: Входящие HTTP-запросы могут обрабатываться серией промежуточного ПО и конечными операциями. Например: Logger, Authorization, GZIP, конечная операция с БД.
  • Обработка сбоев: Gin может перехватить панику, возникшую во время HTTP-запроса, и восстановить её. Таким образом, ваш сервер будет всегда доступен.
  • Валидация JSON: Gin может разбирать и проверять запросы JSON, например, проверять наличие требуемых значений.
  • Группы маршрутов: Лучшая организация маршрутов. Требуется ли авторизация, разные версии API... Кроме того, эти группы могут быть неограниченно вложены без снижения производительности.
  • Управление ошибками: Gin предоставляет удобный способ сбора всех ошибок, возникающих во время HTTP-запроса. В конечном итоге промежуточное ПО может записать их в файл журнала, базу данных и отправить по сети.
  • Встроенный рендеринг: Gin предоставляет простой в использовании API для рендеринга JSON, XML и HTML.
  • Расширяемость: Создание нового промежуточного ПО очень просто.

Установка

На текущий момент 2022/11/22 минимальная поддерживаемая версия Go для Gin — 1.16. Рекомендуется использовать go mod для управления зависимостями проекта.

powershell
go get -u github.com/gin-gonic/gin

Импорт

go
import "github.com/gin-gonic/gin"

Быстрый старт

go
package main

import (
   "github.com/gin-gonic/gin"
   "net/http"
)

func main() {
   engine := gin.Default() // Создание движка gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // Запуск сервера, по умолчанию слушает localhost:8080
}

Запрос URL

http
GET localhost:8080/ping

Ответ

http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18

{
  "message": "pong"
}

Документация

В официальной документации Gin не так много руководств, большинство из них — это просто введения, базовое использование и примеры. Однако под организацией gin-gonic/ есть репозиторий gin-gonic/examples — это репозиторий примеров Gin, поддерживаемый сообществом. Всё на английском языке, обновления не очень частые, автор постепенно изучал фреймворк gin именно отсюда.

Адрес репозитория примеров: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Перед началом рекомендуется ознакомиться с HttpRouter: HttpRouter

Парсинг параметров

Парсинг параметров в gin поддерживает три способа: параметры маршрута, URL-параметры, параметры формы. Ниже приведено подробное объяснение с примерами кода, достаточно просто и понятно.

Параметры маршрута

Параметры маршрута фактически инкапсулируют функциональность парсинга параметров HttpRouter. Использование в основном совпадает с HttpRouter.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser/:username/:userid", FindUser)
   e.GET("/downloadFile/*filepath", UserPage)

   log.Fatalln(e.Run(":8080"))
}

// Пример именованных параметров
func FindUser(c *gin.Context) {
   username := c.Param("username")
   userid := c.Param("userid")
   c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}

// Пример параметров пути
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

Пример 1

bash
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
username is jack
 userid is 001

Пример 2

bash
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'
filepath is  /img/fruit.png

URL-параметры

Традиционные URL-параметры, формат: /url?key=val&key1=val1&key2=val2.

go
package main

import (
   "github.com/gin-gonic/gin"
   "log"
   "net/http"
)

func main() {
   e := gin.Default()
   e.GET("/findUser", FindUser)
   log.Fatalln(e.Run(":8080"))
}

func FindUser(c *gin.Context) {
   username := c.DefaultQuery("username", "defaultUser")
   userid := c.Query("userid")
   c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}

Пример 1

bash
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'
username is jack
userid is 001

Пример 2

bash
curl --location --request GET '127.0.0.1:8080/findUser'
username is defaultUser
userid is

Параметры формы

Типы содержимого формы обычно: application/json, application/x-www-form-urlencoded, application/xml, multipart/form-data.

go
package main

import (
  "github.com/gin-gonic/gin"
  "net/http"
)

func main() {
  e := gin.Default()
  e.POST("/register", RegisterUser)
  e.POST("/update", UpdateUser)
  e.Run(":8080")
}

func RegisterUser(c *gin.Context) {
  username := c.PostForm("username")
  password := c.PostForm("password")
  c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
  var form map[string]string
  c.ShouldBind(&form)
  c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}

Пример 1: Использование form-data

bash
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'
successfully registered,your username is [jack],password is [123456]

Метод PostForm по умолчанию разбирает формы типов application/x-www-form-urlencoded и multipart/form-data.

Пример 2: Использование json

bash
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"username",
    "password":"123456"
}'
successfully update,your username is [username],password is [123456]

Парсинг данных

В большинстве случаев мы используем структуры для переноса данных вместо прямого парсинга параметров. В gin методы для привязки данных — это в основном Bind() и ShouldBind(). Разница между ними в том, что первый внутри вызывает ShouldBind(), и при возврате err сразу отвечает 400, второй же этого не делает. Если требуется более гибкая обработка ошибок, рекомендуется использовать последний. Эти две функции автоматически определяют способ парсинга на основе content-type запроса.

go
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // Вызывает ShouldBindWith()
  if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // Сразу отвечает 400 badrequest
    return err
  }
  return nil
}

Для самостоятельного выбора можно использовать BindWith() и ShouldBindWith(), например:

go
c.MustBindWith(obj, binding.JSON) // json
c.MustBindWith(obj, binding.XML) // xml

Gin поддерживает следующие типы привязки:

go
var (
   JSON          = jsonBinding{}
   XML           = xmlBinding{}
   Form          = formBinding{}
   Query         = queryBinding{}
   FormPost      = formPostBinding{}
   FormMultipart = formMultipartBinding{}
   ProtoBuf      = protobufBinding{}
   MsgPack       = msgpackBinding{}
   YAML          = yamlBinding{}
   Uri           = uriBinding{}
   Header        = headerBinding{}
   TOML          = tomlBinding{}
)

Пример

go
package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "net/http"
)

type LoginUser struct {
  Username string `binding:"required" json:"username" form:"username" uri:"username"`
  Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
  e := gin.Default()
  e.POST("/loginWithJSON", Login)
  e.POST("/loginWithForm", Login)
  e.GET("/loginWithQuery/:username/:password", Login)
  e.Run(":8080")
}

func Login(c *gin.Context) {
  var login LoginUser
    // Использование ShouldBind для автоматического определения gin
  if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
    c.String(http.StatusOK, "login successfully !")
  } else {
    c.String(http.StatusBadRequest, "login failed !")
  }
  fmt.Println(login)
}

Привязка JSON-данных

bash
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"root",
    "password":"root"
}'
login successfully !

Привязка данных формы

bash
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
login successfully !

Привязка URL-данных

bash
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
login failed !

Здесь возникает ошибка, потому что content-type пустой, невозможно определить, как выполнять парсинг данных. Поэтому при использовании URL-параметров следует вручную указать способ парсинга, например:

go
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
   c.String(http.StatusOK, "login successfully !")
} else {
   fmt.Println(err)
   c.String(http.StatusBadRequest, "login failed !")
}

Многократная привязка

Обычно методы вызывают c.Request.Body для привязки данных, но нельзя вызывать этот метод несколько раз, например c.ShouldBind не может быть переиспользован. Если требуется многократная привязка, можно использовать c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // Чтение c.Request.Body и сохранение результата в контексте.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // В этот момент переиспользование тела, сохранённого в контексте.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // Можно принимать другие форматы
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith сохраняет тело в контексте перед привязкой. Это незначительно влияет на производительность. Если привязка выполняется за один вызов, не используйте этот метод. Только некоторые форматы требуют этой функции, такие как JSON, XML, MsgPack, ProtoBuf. Для других форматов, таких как Query, Form, FormPost, FormMultipart, можно многократно вызывать c.ShouldBind() без каких-либо потерь производительности.

Валидация данных

Встроенный инструмент валидации в gin — это github.com/go-playground/validator/v10. Использование практически не отличается. Validator

Простой пример

go
type LoginUser struct {
   Username string `binding:"required"  json:"username" form:"username" uri:"username"`
   Password string `binding:"required" json:"password" form:"password" uri:"password"`
}

func main() {
   e := gin.Default()
   e.POST("/register", Register)
   log.Fatalln(e.Run(":8080"))
}

func Register(ctx *gin.Context) {
   newUser := &LoginUser{}
   if err := ctx.ShouldBind(newUser); err == nil {
      ctx.String(http.StatusOK, "user%+v", *newUser)
   } else {
      ctx.String(http.StatusBadRequest, "invalid user,%v", err)
   }
}

Тест

curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"jack1"

}'

Вывод

invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

TIP

Следует отметить, что в gin тег валидатора — binding, а при отдельном использовании validator тег — validator.

Ответы данными

Ответ данными — это последний шаг в обработке интерфейса. Бэкенд обрабатывает все данные и возвращает их вызывающему через протокол HTTP. Gin предоставляет богатую встроенную поддержку ответов данными, использование простое и понятное, очень легко освоить.

Простой пример

go
func Hello(c *gin.Context) {
    // Возврат данных в формате строки, http.StatusOK — код состояния 200, данные "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

Рендеринг HTML

TIP

При загрузке файлов путь по умолчанию — путь проекта, то есть путь, где находится файл go.mod. index.html в примерах ниже находится в корне проекта. Однако в обычных случаях такие файлы шаблонов не размещаются в корне, а хранятся в папке статических ресурсов.

go
func main() {
   e := gin.Default()
    // Загрузка HTML-файла, также можно использовать Engine.LoadHTMLGlob()
   e.LoadHTMLFiles("index.html")
   e.GET("/", Index)
   log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
   c.HTML(http.StatusOK, "index.html", gin.H{})
}

Тест

curl --location --request GET 'http://localhost:8080/'

Ответ

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>GinLearn</title>
  </head>

  <body>
    <h1>Hello World!</h1>
    <h1>This is a HTML Template Render Example</h1>
  </body>
</html>

Быстрые ответы

Ранее часто использовался метод context.String() для ответов данными. Это самый原始ный метод ответа, возвращающий строку. В gin также встроено множество методов для быстрых ответов, например:

go
// Запись заголовка ответа с помощью Render и рендеринг данных
func (c *Context) Render(code int, r render.Render)

// Рендеринг HTML-шаблона, name — путь к html, obj — содержимое
func (c *Context) HTML(code int, name string, obj any)

// Рендеринг данных в виде красивого отформатированного JSON. Обычно не рекомендуется использовать этот метод, так как это увеличивает затраты на передачу.
func (c *Context) IndentedJSON(code int, obj any)

// Безопасный JSON, может предотвратить JSON-угон. Подробнее: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// Рендеринг в формате JSONP
func (c *Context) JSONP(code int, obj any)

// Рендеринг в формате JSON
func (c *Context) JSON(code int, obj any)

// Рендеринг в формате JSON с преобразованием unicode в ASCII
func (c *Context) AsciiJSON(code int, obj any)

// Рендеринг в формате JSON без экранирования специальных HTML-символов
func (c *Context) PureJSON(code int, obj any)

// Рендеринг в формате XML
func (c *Context) XML(code int, obj any)

// Рендеринг в формате YML
func (c *Context) YAML(code int, obj any)

// Рендеринг в формате TOML
func (c *Context) TOML(code int, obj interface{})

// Рендеринг в формате ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)

// Рендеринг в формате String
func (c *Context) String(code int, format string, values ...any)

// Перенаправление в определённое место
func (c *Context) Redirect(code int, location string)

// Запись данных в поток ответа
func (c *Context) Data(code int, contentType string, data []byte)

// Чтение потока через reader и запись в поток ответа
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// Эффективная запись файла в поток ответа
func (c *Context) File(filepath string)

// Эффективная запись потока файла из fs в поток ответа
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// Эффективная запись потока файла из fs в поток ответа, при этом клиент скачает файл с указанным именем
func (c *Context) FileAttachment(filepath, filename string)

// Запись потока серверной отправки в поток ответа
func (c *Context) SSEvent(name string, message any)

// Отправка потокового ответа и возврат булевого значения для определения, отключился ли клиент во время потока
func (c *Context) Stream(step func(w io.Writer) bool) bool

Для большинства приложений чаще всего используется context.JSON, другие используются реже. Здесь не будут приведены примеры, так как они достаточно просты и понятны, в основном это просто вызовы методов.

Асинхронная обработка

В gin асинхронная обработка требует использования goroutine. Использование очень простое.

go
// copy возвращает копию текущего Context для безопасного использования вне области действия текущего Context, можно передать в goroutine
func (c *Context) Copy() *Context
go
func main() {
  e := gin.Default()
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Hello(c *gin.Context) {
  ctx := c.Copy()
  go func() {
    // Дочерняя горутин должна использовать копию Context, не следует использовать оригинальный Context
    log.Println("Функция асинхронной обработки: ", ctx.HandlerNames())
  }()
  log.Println("Функция обработки интерфейса: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Тест

go
curl --location --request GET 'http://localhost:8080/hello'

Вывод

go
2022/12/21 13:33:47 Функция асинхронной обработки:  []
2022/12/21 13:33:47 Функция обработки интерфейса:  [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 |     11.1927ms |             ::1 | GET      "/hello"

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

Передача файлов

Передача файлов — это неотъемлемая функция веб-приложений. Поддержка Gin в этом отношении также очень проста по сути, но в основном процесс почти такой же, как у нативного net/http. Процесс заключается в чтении потока файла из тела запроса, а затем сохранении его локально.

Загрузка одного файла

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
  // Получение файла
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Сохранение локально
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Возврат результата
  ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

Тест

curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'

Результат

upload a.jpg size:1424 byte successfully!

TIP

Обычно для загрузки файлов Method указывается как POST. Некоторые компании могут предпочесть использовать PUT. Первый — простой HTTP-запрос, второй — сложный HTTP-запрос. Конкретные различия не обсуждаются. При использовании последнего, особенно в проектах с разделением фронтенда и бэкенда, требуется соответствующая обработка CORS. Конфигурация по умолчанию в Gin не поддерживает CORS Настройка CORS.

Загрузка нескольких файлов

go
func main() {
   e := gin.Default()
   e.POST("/upload", uploadFile)
   e.POST("/uploadFiles", uploadFiles)
   log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
  // Получение разобранной gin multipart формы
  form, _ := ctx.MultipartForm()
  // Получение списка файлов по ключу
  files := form.File["files"]
  // Перебор списка файлов, сохранение локально
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // Возврат результата
  ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

Тест

curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'

Вывод

upload 3 files successfully!

Скачивание файлов

Что касается части скачивания файлов, Gin ещё раз инкапсулировал API стандартной библиотеки, сделав скачивание файлов чрезвычайно простым.

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  e.POST("/uploadFiles", uploadFiles)
  e.GET("/download/:filename", download)
  log.Fatalln(e.Run(":8080"))
}

func download(ctx *gin.Context) {
    // Получение имени файла
  filename := ctx.Param("filename")
    // Возврат соответствующего файла
  ctx.FileAttachment(filename, filename)
}

Тест

curl --location --request GET 'http://localhost:8080/download/a.jpg'

Результат

Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMT

Не кажется ли это слишком простым? Попробуйте написать процесс самостоятельно без методов фреймворка:

go
func download(ctx *gin.Context) {
   // Получение параметра
   filename := ctx.Param("filename")

   // Объекты ответа и запроса
   response, request := ctx.Writer, ctx.Request
   // Запись заголовка ответа
   // response.Header().Set("Content-Type", "application/octet-stream")  Передача файла в виде двоичного потока
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Безопасное экранирование имени файла
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // Кодировка передачи
   http.ServeFile(response, request, filename)
}

На самом деле net/http также достаточно хорошо инкапсулирован.

TIP

Можно установить максимальный объём памяти для передачи файлов через Engine.MaxMultipartMemory. По умолчанию 32 << 20 // 32 MB.

Управление маршрутами

Управление маршрутами — очень важная часть системы. Необходимо убедиться, что каждый запрос правильно сопоставлен с соответствующей функцией.

Группы маршрутов

Создание группы маршрутов — это классификация интерфейсов. Интерфейсы разных категорий соответствуют разным функциям, что также упрощает управление.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Предположим, у нас есть четыре вышеуказанных интерфейса. Пока не будем заботиться об их внутренней реализации. Hello, Login — одна группа, Update, Delete — другая группа.

go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

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

go
func main() {
  e := gin.Default()
  v1 := e.Group("v1")
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("v2")
  {
    v2.POST("/update", Update)
    v2.DELETE("/delete", Delete)
  }
}

Мы разделили их на две группы v1, v2. Фигурные скобки {} служат только для соглашения, обозначая, что обработчики, зарегистрированные внутри фигурных скобок, принадлежат одной группе маршрутов. Они не имеют функционального назначения. Аналогично, gin поддерживает вложенные группы. Метод такой же, как в примере выше, здесь не будет демонстрироваться.

Маршрут 404

Структура Engine в gin предоставляет метод NoRoute для настройки обработки при访问 несуществующего URL. Разработчик может записать логику в этот метод, чтобы он автоматически вызывался при ненахождении маршрута. По умолчанию возвращается код состояния 404.

go
func (engine *Engine) NoRoute(handlers ...HandlerFunc)

Возьмём предыдущий пример:

go
func main() {
   e := gin.Default()
   v1 := e.Group("v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   // Регистрация обработчика
   e.NoRoute(func(context *gin.Context) { // Это демонстрация, не следует напрямую возвращать HTML-код в производственной среде
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Отправим любой запрос:

curl --location --request GET 'http://localhost:8080/'
<h1>404 Page Not Found</h1>

Маршрут 405

В кодах состояния HTTP 405 означает, что текущий метод запроса не разрешён. Gin предоставляет следующий метод:

go
func (engine *Engine) NoMethod(handlers ...HandlerFunc)

для регистрации обработчика, который автоматически вызывается при возникновении, при условии установки Engine.HandleMethodNotAllowed = true.

go
func main() {
   e := gin.Default()
   // Необходимо установить в true
   e.HandleMethodNotAllowed = true
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   e.NoRoute(func(context *gin.Context) {
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   // Регистрация обработчика
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "method not allowed")
   })
   log.Fatalln(e.Run(":8080"))
}

После настройки заголовок по умолчанию в gin не поддерживает запросы OPTION. Протестируем:

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
method not allowed

Настройка успешно завершена.

Перенаправление

Перенаправление в gin очень простое, достаточно вызвать метод gin.Context.Redirect().

go
func main() {
  e := gin.Default()
  e.GET("/", Index)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

func Index(c *gin.Context) {
  c.Redirect(http.StatusMovedPermanently, "/hello")
}

func Hello(c *gin.Context) {
  c.String(http.StatusOK, "hello")
}

Тест

curl --location --request GET 'http://localhost:8080/'

Вывод

hello

Промежуточное ПО

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

Промежуточное ПО по сути всё ещё является обработчиком интерфейсов:

go
// HandlerFunc определяет обработчик, используемый промежуточным ПО gin как возвращаемое значение.
type HandlerFunc func(*Context)

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

go
func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

Просмотрев исходный код gin, в функции Default возвращаемый по умолчанию Engine использует два промежуточных ПО Logger(), Recovery(). Если не хотите использовать промежуточное ПО по умолчанию, можно использовать gin.New().

Глобальное промежуточное ПО

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

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("Глобальное промежуточное ПО выполнено...")
   }
}

Сначала создаём функцию замыкания для создания промежуточного ПО, затем регистрируем глобальное промежуточное ПО через Engine.Use().

go
func main() {
   e := gin.Default()
   // Регистрация глобального промежуточного ПО
   e.Use(GlobalMiddleware())
   v1 := e.Group("/v1")
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      v2.POST("/update", Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Тест

curl --location --request GET 'http://localhost:8080/v1/hello'

Вывод

[GIN-debug] Listening and serving HTTP on :8080
Глобальное промежуточное ПО выполнено...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

Локальное промежуточное ПО

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

go
func main() {
   e := gin.Default()
   // Регистрация глобального промежуточного ПО
   e.Use(GlobalMiddleware())
   // Регистрация локального промежуточного ПО группы маршрутов
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // Регистрация локального промежуточного ПО отдельного маршрута
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Тест

curl --location --request POST 'http://localhost:8080/v2/update'

Вывод

Глобальное промежуточное ПО выполнено...
Локальное промежуточное ПО выполнено
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

Принцип работы промежуточного ПО

Использование и кастомизация промежуточного ПО в Gin очень просты. Внутренний принцип также достаточно прост. Для дальнейшего обучения нужно немного понять внутренний принцип. Промежуточное ПО в Gin фактически использует паттерн цепочки ответственности. В Context поддерживается HandlersChain, по сути это []HandlerFunc, и index типа int8. В методе Engine.handlerHTTPRequest(c *Context) есть код, указывающий на процесс вызова: после того как gin находит соответствующий маршрут в дереве маршрутов, вызывается метод Next().

go
if value.handlers != nil {
   // Присваивание цепочки вызовов Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // Вызов промежуточного ПО
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

Ключевым является вызов Next(). Next() перебирает HandlerFunc в handlers маршрута и выполняет их. Здесь можно увидеть, что роль index — запись позиции вызова промежуточного ПО. Среди них функция интерфейса, зарегистрированная для соответствующего маршрута, также находится в handlers. Вот почему ранее говорилось, что интерфейс также является промежуточным ПО.

go
func (c *Context) Next() {
   // Увеличение на 1 при входе是为了 избежать рекурсивного бесконечного цикла, значение по умолчанию -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // Выполнение HandlerFunc
      c.handlers[c.index](c)
      // После выполнения, увеличение index на 1
      c.index++
   }
}

Изменим логику Hello(), чтобы проверить, действительно ли это так:

go
func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

Результат вывода:

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

Можно увидеть, что порядок цепочки вызовов промежуточного ПО: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello. Последний элемент цепочки вызовов — это фактически функция интерфейса, которую нужно выполнить, всё перед ним — промежуточное ПО.

TIP

При регистрации локального маршрута есть следующее утверждение:

go
finalSize := len(group.Handlers) + len(handlers) // Общее количество промежуточного ПО
assert1(finalSize < int(abortIndex), "too many handlers")

Где abortIndex int8 = math.MaxInt8 >> 1 равно 63, то есть при использовании системы количество зарегистрированных маршрутов не должно превышать 63.

Промежуточное ПО таймера

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

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // Запись времени начала
      start := time.Now()
      // Выполнение последующей цепочки вызовов
      context.Next()
      // Вычисление интервала времени
      duration := time.Since(start)
      // Вывод в наносекундах для наблюдения результата
      fmt.Println("Время запроса: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // Регистрация глобального промежуточного ПО, промежуточного ПО таймера
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // Регистрация локального промежуточного ПО группы маршрутов
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // Регистрация локального промежуточного ПО отдельного маршрута
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

Тест

curl --location --request GET 'http://localhost:8080/v1/hello'

Вывод

Время запроса:  517600

Простое промежуточное ПО таймера уже написано. В дальнейшем можно самостоятельно написать некоторые более практичные промежуточные ПО.

Конфигурация сервиса

Использование только конфигурации по умолчанию недостаточно. В большинстве случаев требуется изменить множество конфигураций сервиса для достижения требований.

HTTP-конфигурация

Можно настроить через создание Server в net/http. Gin также поддерживает использование как нативный API.

go
func main() {
   router := gin.Default()
   server := &http.Server{
      Addr:           ":8080",
      Handler:        router,
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
   }
   log.Fatal(server.ListenAndServe())
}

Конфигурация статических ресурсов

Статические ресурсы в прошлом были неотъемлемой частью сервера. Хотя их доля использования постепенно уменьшается, всё ещё много систем используют монолитную архитектуру.

Gin предоставляет три метода для загрузки статических ресурсов:

go
// Загрузка определённой папки статических файлов
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// Загрузка определённого fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// Загрузка определённого статического файла
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath — это относительный путь, сопоставленный с URL веб-страницы. root — фактический путь файла в проекте.

Предположим, структура проекта следующая:

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // Загрузка каталога статических файлов
   router.Static("/static", "./static")
   // Загрузка каталога статических файлов
   router.StaticFS("/view", http.Dir("view"))
   // Загрузка статического файла
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

Настройка CORS

Gin сам по себе не обрабатывает настройку CORS. Требуется самостоятельно написать промежуточное ПО для реализации соответствующих требований. Сложность невелика, те, кто немного знаком с протоколом HTTP, обычно могут это написать. Логика в основном одна и та же.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // В производственной среде серверы обычно не заполняют *, следует указать конкретное доменное имя
         c.Header("Access-Control-Allow-Origin", origin)
         // Разрешённые HTTP METHOD
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // Разрешённые заголовки запроса
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // Разрешённые заголовки ответа для доступа клиента
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Требуется ли информация об аутентификации Credentials, может быть cookies, заголовки authorization или сертификаты клиента TLS
         // При установке в true, Access-Control-Allow-Origin не может быть *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // Пропуск запроса OPTION, но не выполнение последующих методов
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // Пропуск
      c.Next()
   }
}

Зарегистрировать промежуточное ПО как глобальное промежуточное ПО.

Управление сессиями

В нынешнюю эпоху существуют три популярных типа управления веб-сессиями: cookie, session, JWT.

Информация в cookie хранится в браузере в виде пар ключ-значение, и данные可以直接 увидеть в браузере.

Преимущества:

  • Простая структура
  • Постоянное хранение данных

Недостатки:

  • Ограниченный размер
  • Хранение в открытом виде
  • Подверженность CSRF-атакам
go
import (
    "fmt"

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

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

         // Получение соответствующего cookie
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // Установка cookie. Параметры: key, val, время существования, путь, домен, разрешать ли другим доступ к cookie через js, только http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

Чистые cookie использовались чаще пять-шесть лет назад, но автор редко использует чистые cookie для управления сессиями, это действительно небезопасно.

Session

Session хранится на сервере, затем отправляется cookie, хранящийся в браузере. В cookie хранится session_id. После этого при каждом запросе сервер через session_id может получить соответствующую информацию session.

Преимущества:

  • Хранение на сервере увеличивает безопасность, удобно управлять

Недостатки:

  • Хранение на сервере увеличивает нагрузку на сервер, снижает производительность
  • Идентификация на основе cookie небезопасна
  • Информация об аутентификации не синхронизируется в распределённых ситуациях

Session и Cookie неразрывны. При использовании Session по умолчанию подразумевается использование Cookie. Gin по умолчанию не поддерживает Session, потому что Cookie — это содержимое протокола Http, а Session — нет. Однако есть поддержка стороннего промежуточного ПО, достаточно установить зависимость. Адрес репозитория: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

Поддерживает cookie, Redis, MongoDB, GORM, PostgreSQL.

go
func main() {
   r := gin.Default()
   // Создание движка хранения на основе Cookie
   store := cookie.NewStore([]byte("secret"))
   // Установка промежуточного ПО Session, mysession — имя session, также имя cookie
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // Инициализация session
      session := sessions.Default(c)
      var count int
      // Получение значения
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // Установка
      session.Set("count", count)
      // Сохранение
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Обычно не рекомендуется хранить Session через Cookie, рекомендуется использовать Redis. Другие примеры можно узнать в официальном репозитории.

JWT

Преимущества:

  • На основе JSON, универсально для многих языков
  • Можно хранить нечувствительную информацию
  • Занимает мало места, удобен для передачи
  • Серверу не нужно хранить, выгодно для распределённого расширения

Недостатки:

  • Проблема обновления Token
  • После выдачи нельзя активно контролировать

С начала фронтенд-революции фронтенд-программисты больше не просто «пишут страницы». Тренд разделения фронтенда и бэкенда становится всё сильнее. JWT наиболее подходит для управления сессиями в системах с разделением фронтенда и бэкенда и распределённых системах, имеет большие естественные преимущества. Учитывая, что JWT полностью вышел за рамки содержания Gin и не имеет поддержки промежуточного ПО, потому что JWT сам по себе не ограничен каким-либо фреймворком или языком, здесь не будет подробного объяснения. Можно перейти к другому документу: JWT

Управление журналами

Промежуточное ПО журнала по умолчанию в Gin использует os.Stdout, имеет только базовые функции, поскольку Gin фокусируется только на веб-сервисах. В большинстве случаев следует использовать более зрелые фреймворки журналов, но это не входит в обсуждение этой главы. Расширяемость Gin очень высока, можно легко интегрировать другие фреймворки. Здесь обсуждается только его встроенный сервис журналов.

Цвет консоли

go
gin.DisableConsoleColor() // Отключение цвета журнала консоли

Кроме разработки, в большинстве случаев не рекомендуется включать этот элемент.

Запись журнала в файл

go
func main() {
  e := gin.Default()
    // Отключение цвета консоли
  gin.DisableConsoleColor()
    // Создание двух файлов журнала
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // Одновременная запись в два файла журнала
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

Встроенная поддержка журналов в gin позволяет записывать в несколько файлов, но содержимое одинаковое, использование не очень удобное, и запросы журнала не записываются в файлы.

go
func main() {
  router := gin.New()
  // Промежуточное ПО LoggerWithFormatter записывает журнал в gin.DefaultWriter
  // По умолчанию gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        // TODO логика записи в соответствующий файл
        ......
    // Вывод пользовательского формата
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
        param.ClientIP,
        param.TimeStamp.Format(time.RFC1123),
        param.Method,
        param.Path,
        param.Request.Proto,
        param.StatusCode,
        param.Latency,
        param.Request.UserAgent(),
        param.ErrorMessage,
    )
  }))
  router.Use(gin.Recovery())
  router.GET("/ping", func(c *gin.Context) {
    c.String(200, "pong")
  })
  router.Run(":8080")
}

Через пользовательское промежуточное ПО можно реализовать запись журнала в файлы.

Формат журнала отладки маршрутов

Здесь изменяется только журнал вывода информации о маршрутах при запуске:

go
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("Маршрут %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

Вывод

2022/12/21 17:19:13 Маршрут GET /hello main.Hello 3

Заключение: Gin является одним из самых простых для изучения веб-фреймворков языка Go, потому что Gin действительно достиг минимизации ответственности, просто отвечает за веб-сервисы. Остальная логика аутентификации, кэширование данных и другие функции передаются разработчикам для самостоятельного выполнения. По сравнению с большими и полными фреймворками, лёгкий и简洁ный Gin более подходит и должен быть изучен начинающими, потому что Gin не принудительно использует какие-либо спецификации. Как构建ировать проект, какую структуру принять — всё это нужно самостоятельно обдумывать. Для начинающих это больше развивает способности.

Golang by www.golangdev.cn edit