Облегченный веб-фреймворк Gin Структурный анализ

Go Gin

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

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

Hello World

Для Hello World среды Gin требуется всего 10 строк кода, что немного больше, чем для большинства динамических языков сценариев.

package main

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

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

gin.H в коде — это сокращение для map[string]interface{}, которое более лаконично.

type H map[string]interface{}

gin.Engine

Engine — это самая важная структура данных Gin framework и точка входа фреймворка. Мы используем объект Engine для определения информации о маршрутизации службы, сборки плагинов и запуска служб. Точно так же, как Engine означает «двигатель» на китайском языке, это основной механизм фреймворка, и весь веб-сервис управляется им.

Движок — это прецизионное устройство и его структура очень сложна, но объект Engine очень прост, потому что самая важная часть движка — базовый HTTP-сервер использует HTTP-сервер, построенный на языке Go, он более удобен в использовании. .

Функция gin.Default () создаст объект Engine по умолчанию, который содержит два общих плагина по умолчанию, а именно Logger и Recovery. Logger используется для вывода журналов запросов. Recovery гарантирует, что при панике одного запроса журнал стека исключений записывается и вывод Унифицированный ответ об ошибке.

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

дерево маршрутизации

В среде Gin правила маршрутизации разделены на 9 префиксных деревьев, каждый HTTP-метод соответствует «префиксному дереву», узлы дерева иерархически разделены в соответствии с символом / в URL-адресе, а URL-адрес поддерживает сопоставление имен. в форме :name.Подстановочные знаки пути в форме *subpath также поддерживаются.

// 匹配单节点 named
pattern = /book/:id
match /book/123
nomatch /book/123/10
nomatch /book/

// 匹配子节点 catchAll mode
/book/*subpath
match /book/
match /book/123
match /book/123/10

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

type Engine struct {
  ...
  trees methodTrees
  ...
}

type methodTrees []methodTree

type methodTree struct {
	method string
	root   *node  // 树根
}

type node struct {
  path string // 当前节点的路径
  ...
  handlers HandlersChain // 请求处理链
  ...
}

type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc

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

func (e *Engine) addRoute(method, path string, handlers HandlersChain)

gin.RouterGroup

RouterGroup — это оболочка вокруг дерева маршрутизации, и все правила маршрутизации в конечном итоге управляются ею. Структура Engine наследует RouterGroup, поэтому Engine напрямую имеет все функции управления маршрутизацией от RouterGroup. Вот почему в примере с Hello World объект Engine можно использовать непосредственно для определения правил маршрутизации. В то же время объект RouteGroup также будет содержать указатель Engine, так что Engine и RouteGroup имеют отношение «у вас есть я, а у меня есть вы».

type Engine struct {
  RouterGroup
  ...
}

type RouterGroup struct {
  ...
  engine *Engine
  ...
}

RouterGroup реализует интерфейс IRouter и предоставляет ряд методов маршрутизации, которые в конечном итоге подключают обработчик запросов к дереву маршрутизации, вызывая метод Engine.addRoute.

GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
// 匹配所有 HTTP Method
Any(string, ...HandlerFunc) IRoutes

Внутри RouterGroup есть атрибут пути префикса, который добавит этот префикс ко всем подпутям и поместит их в дерево маршрутизации. С помощью этого пути префикса может быть реализована функция группировки URL-адресов. Префикс пути объекта RouterGroup, встроенного в объект Engine, — это /, который представляет корневой путь. RouterGroup поддерживает вложенность групп, с помощью метода Group можно подвешивать группы под группу, поэтому потомки бесконечны.

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

	v1 := router.Group("/v1")
	{
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	v2 := router.Group("/v2")
	{
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}

В приведенном выше примере фактически использовалась вложенность групп, поскольку объект RouterGroup в объекте Engine является группой первого уровня, которая является корневой группой, а v1 и v2 являются подгруппами корневой группы.

gin.Context

Этот объект содержит контекстную информацию запроса и является входным параметром всех обработчиков запросов.

type HandlerFunc func(*Context)

type Context struct {
  ...
  Request *http.Request // 请求对象
  Writer ResponseWriter // 响应对象
  Params Params // URL匹配参数
  ...
  Keys map[string]interface{} // 自定义上下文信息
  ...
}

Объект Context предоставляет очень богатый метод для получения информации о контексте текущего запроса.Если вам нужно получить параметры URL, файлы cookie и заголовки в запросе, вы можете получить их через объект Context. Эта серия методов по сути представляет собой оболочку вокруг объекта http.Request.

// 获取 URL 匹配参数  /book/:id
func (c *Context) Param(key string) string
// 获取 URL 查询参数 /book?id=123&page=10
func (c *Context) Query(key string) string
// 获取 POST 表单参数
func (c *Context) PostForm(key string) string
// 获取上传的文件对象
func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
// 获取请求Cookie
func (c *Context) Cookie(name string) (string, error) 
...

Объект Context предоставляет множество встроенных форматов ответов, JSON, HTML, Protobuf, MsgPack, Yaml и т. д. Он создаст отдельный рендерер для каждой формы. Обычно этих встроенных рендереров достаточно для большинства сцен, если вы чувствуете, что этого недостаточно, вы также можете настроить рендерер.

func (c *Context) JSON(code int, obj interface{})
func (c *Context) Protobuf(code int, obj interface{})
func (c *Context) YAML(code int, obj interface{})
...
// 自定义渲染
func (c *Context) Render(code int, r render.Render)

// 渲染器通用接口
type Render interface {
	Render(http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}

Все рендереры в конечном итоге должны вызывать встроенный http.ResponseWriter (Context.Writer), чтобы преобразовать объект ответа в поток байтов и записать его в сокет.

type ResponseWriter interface {
 // 容纳所有的响应头
 Header() Header
 // 写Body
 Write([]byte) (int, error)
 // 写Header
 WriteHeader(statusCode int)
}

Плагины и цепочки запросов

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

type node struct {
  path string // 当前节点的路径
  ...
  handlers HandlersChain // 请求处理链
  ...
}
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc

Это связано с тем, что Gin предоставляет подключаемые модули, только хвост цепочки функций — это бизнес-обработка, а передняя часть — все подключаемые функции. В Gin форма плагина и функции бизнес-обработки одинакова, обе они func(*Context). Когда мы определяем маршруты, Gin будет объединять функции плагинов и функции бизнес-обработки вместе, чтобы сформировать структуру цепочки.

type Context struct {
  ...
  index uint8 // 当前的业务逻辑位于函数链的位置
  handlers HandlersChain // 函数链
  ...
}

// 挨个调用链条中的处理函数
func (c *Context) Next() {
	c.index++
	for s := int8(len(c.handlers)); c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

Когда Gin получает клиентский запрос, он находит соответствующую цепочку обработки, создает объект Context, а затем вызывает свой метод Next(), чтобы формально ввести весь процесс обработки запроса.

Gin также поддерживает метод Abort() для прерывания выполнения цепочки запросов.Его принцип заключается в настройке Context.index на относительно большое число, чтобы цикл вызова в методе Next() завершался немедленно. Следует отметить, что метод Abort() не прерывает поток выполнения из-за паники.После выполнения метода Abort() логика кода текущей функции продолжит выполняться.

const abortIndex = 127
func (c *Context) Abort() {
	c.index = abortIndex
}

func SomePlugin(c *Context) {
  ...
  if condition {
    c.Abort()
    // continue executing
  }
  ...
}

Если в подключаемом модуле явно вызывается метод Next(), то он изменяет обычный последовательный поток выполнения на луковичный вложенный поток выполнения. Чтобы понять это под другим углом, нормальный поток выполнения состоит в том, что последующий процессор выполняется в конце предыдущего процессора, в то время как вложенный поток выполнения позволяет следующему процессору выполняться на полпути от предыдущего процессора и ждать последующей обработки. процессор завершает выполнение, он возвращается к предыдущему процессору для продолжения выполнения.

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

RouterGroup предоставляет метод Use() для регистрации подключаемых модулей, поскольку RouterGroup является послойным, маршруты на разных уровнях могут регистрировать разные подключаемые модули, и, в конечном счете, разные узлы маршрутизации имеют разные цепочки функций обработки.

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

// 注册 Get 请求
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("GET", relativePath, handlers)
}

func (g *RouterGroup) handle(method, path string, handlers HandlersChain) IRoutes {
 // 合并URL (RouterGroup有URL前缀)
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合并处理链条
 handlers = group.combineHandlers(handlers)
	// 注册路由树
 group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

HTTP-ошибка

Когда путь, соответствующий URL-запросу, не может быть найден в дереве маршрутизации, необходимо обработать ошибку 404 NotFound. Когда путь запроса URL-адреса можно найти в дереве маршрутизации, но метод не совпадает, необходимо обработать ошибку 405 MethodNotAllowed. Объект Engine предоставляет запись для регистрации обработчика для этих двух ошибок.

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

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

Обратите внимание, что эти два обработчика ошибок определены в глобальном объекте Engine, а не в RouterGroup. Для ошибок, отличных от 404 и 405, для их обработки требуется определяемый пользователем плагин. Исключения, вызванные паникой, также должны обрабатываться плагинами.

подача статических файлов

Объект RouterGroup определяет следующие три метода обслуживания статических файлов.

// 服务单个静态文件
StaticFile(relativePath, filePath string) IRoutes
// 服务静态文件目录
Static(relativePath, dirRoot string) IRoutes
// 服务虚拟静态文件系统
StaticFS(relativePath string, fs http.FileSystem) IRoutes

Он отличается от обработчика ошибок тем, что статический файловый сервис висит на RouterGroup и поддерживает вложенность. Среди этих трех методов метод StaticFS является более особенным.Он абстрагирует файловую систему.Вы можете предоставить статическую файловую систему на основе сети или статическую файловую систему на основе памяти. Интерфейс FileSystem также очень прост, предоставляя параметр пути и возвращая файловый объект, который реализует интерфейс File. Различные виртуальные файловые системы используют разный код для реализации файлового интерфейса.

type FileSystem interface {
 Open(path string) (File, error)
}

type File interface {
 io.Closer
 io.Reader
 io.Seeker
 Readdir(count int) ([]os.FileInfo, error)
 Stat() (os.FileInfo, error)
}

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

обработка формы

Когда количество параметров запроса велико, использование методов Context.Query() и Context.PostForm() для получения параметров будет громоздким. Платформа Gin также поддерживает обработку форм, которая напрямую отображает параметры формы в поля структуры.

package main

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

type LoginForm struct {
	User     string `form:"user" binding:"required"`
	Password string `form:"password" binding:"required"`
}

func main() {
	router := gin.Default()
	router.POST("/login", func(c *gin.Context) {
  var form LoginForm
		if c.ShouldBind(&form) == nil {
			if form.User == "user" && form.Password == "password" {
				c.JSON(200, gin.H{"status": "you are logged in"})
			} else {
				c.JSON(401, gin.H{"status": "unauthorized"})
			}
		}
	})
	router.Run(":8080")
}

Когда метод Context.ShouldBind сталкивается с ошибкой проверки, он возвращает объект ошибки, чтобы информировать вызывающую сторону о причине ошибки проверки. Он поддерживает различные типы привязки данных, такие как XML, JSON, Query, Uri, MsgPack, Protobuf и т. д. Какой метод привязки данных использовать, определяется в соответствии с заголовком Content-Type запроса.

func (c *Context) ShouldBind(obj interface{}) error {
 // 获取绑定器
	b := binding.Default(c.Request.Method, c.ContentType())
	// 执行绑定
 return c.ShouldBindWith(obj, b)
}

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

Context.ShouldBind — более мягкий метод проверки, он отвечает только за проверку и передает результат проверки на верхний уровень в виде возвращаемого значения. У Context есть еще один более жесткий метод проверки Context.Bind, который точно такой же, как ShouldBind, разница в том, что при возникновении ошибки проверки он вызывает метод Abort(), чтобы прервать выполнение цепочки вызовов и сообщить об этом клиенту. , Возвращает ошибку HTTP 400 Bad Request.

HTTPS

Gin не поддерживает HTTPS, официальная рекомендация — использовать Nginx для пересылки HTTPS-запросов в Gin.