1. 简介
官网:https://gin-gonic.com/
github仓库地址:https://github.com/gin-gonic/gin
文档地址:https://pkg.go.dev/github.com/gin-gonic/gin
Gin 是一个 Go 语言编写的的 HTTP Web框架,它包含的特性如下:
- 快速:基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能;
- 支持中间件:传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB;
- Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic;
- JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在;
- 路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能;
- 错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送;
- 内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API;
2. 使用
2.1 示例程序
要使用 Gin,需要要求 Go 1.13 及以上版本。
下载安装 Gin:
go get -u github.com/gin-gonic/gin
在代码中使用包:
import "github.com/gin-gonic/gin"
示例代码:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 初始化对象
r.GET("/ping", func(c *gin.Context) { // 注册一个 GET 方法接口
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
执行命令启动程序:
go run main.go
更多官方示例可以参考:https://gin-gonic.com/zh-cn/docs/examples/
2.2 程序启动
直接使用 http.ListenAndServe 启动程序。
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
也可以自定义 HTTP 配置后启动程序。
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
2.3 优雅重启或停止
启动程序前,监听特定的信号,然后在收到信号后退出程序,并在退出程序前,保留一定时间完成现有的请求。
不带有 context 的通知,Go 1.8 及更新版本支持。
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// 创建一个协程启动服务
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %s\n", err)
}
}()
// 监听中断信号
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
// 完成服务器停止前的必要工作
// 设置 5 秒的超时时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 使用 http.Server 内置的 Shutdown() 方法优雅地关机
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}
带有 context 的通知,Go 1.16 及更新版本支持。
package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
// 创建 context 来监听信号
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(10 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// 创建一个协程启动服务
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 监听中断信号
<-ctx.Done()
// 完成服务器停止前的必要工作
// 设置 5 秒的超时时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 使用 http.Server 内置的 Shutdown() 方法优雅地关机
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}
log.Println("Server exiting")
}
2.4 记录日志
Gin 支持指定日志文件路径,然后就可以将输出日志记录到文件里去,也可以配置同时输出到控制台。
func main() {
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
gin.DisableConsoleColor()
// 记录到文件。
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果需要同时将日志写入文件和控制台,请使用以下代码。
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
默认的路由日志格式如下:
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)
以下可以自定义路由日志格式。
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
2.5 将请求体绑定到结构体
要将请求体绑定到结构体中,使用模型绑定。 Gin目前支持JSON、XML、YAML和标准表单值的绑定(foo=bar&boo=baz)。Gin使用 go-playground/validator/v10 进行验证。
使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定,结构体成员变量的标签指定了从哪个请求体参数中绑定。
你也可以指定必须绑定的字段, 如果一个字段的 tag 加上了 binding:"required"
,但绑定时是空值,Gin 会报错。
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
Gin 提供了两类绑定方法:
- Must bind:包含方法 Bind, BindJSON, BindXML, BindQuery, BindYAML,如果发生绑定错误,则请求终止。
- Shoud bind:包含方法 ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML,如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
通过调用 c.ShouldBind 方法将请求体 c.Request.Body 绑定到结构体,但是只能调用一次。
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}
type formB struct {
Bar string `json:"bar" xml:"bar" binding:"required"`
}
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
if errA := c.ShouldBind(&objA); errA == nil { // c.ShouldBind 使用了 c.Request.Body,不可重用。
c.String(http.StatusOK, `the body should be formA`)
} else if errB := c.ShouldBind(&objB); errB == nil { // 因为现在 c.Request.Body 是 EOF,所以这里会报错。
c.String(http.StatusOK, `the body should be formB`)
} else {
}
}
要想多次绑定,可以使用 c.ShouldBindBodyWith。
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil { // 读取 c.Request.Body 并将结果存入上下文。
c.String(http.StatusOK, `the body should be formA`)
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { // 这时, 复用存储在上下文中的 body。
c.String(http.StatusOK, `the body should be formB JSON`)
} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil { // 可以接受其他格式
c.String(http.StatusOK, `the body should be formB XML`)
} else {
}
}
2.6 中间件
需要修改初始化 Engine 对象的方法 Default 改为 New。
// Default 使用 Logger 和 Recovery 中间件
r := gin.Default()
// 替换为 New
r := gin.New()
中间件是在绑定的接口执行前后执行的代码,可以设置全局中间件,可以为某一个路由添加中间件,也可以为路由组设置中间件。
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 全局中间件
// Logger 中间件将日志写入 gin.DefaultWriter,默认是 os.Stdout
r.Use(gin.Logger())
// Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500。
r.Use(gin.Recovery())
// 你可以为每个路由添加任意数量的中间件。
r.GET("/benchmark", benchmarkHandler(), middleware1, middleware2)
// 路由组中间件
authorized := r.Group("/", middleware3)
authorized.Use(middleware4)
{
authorized.POST("/login", middleware5, loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// 嵌套路由组
testing := authorized.Group("testing")
testing.Use(middleware6)
testing.GET("/analytics", analyticsEndpoint)
}
// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}
自定义中间件并使用:
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
c.Set("example", "12345")
c.Next() // 该调用前面的代码在请求之前执行,后面的代码在请求之后执行
latency := time.Since(t)
log.Print(latency)
status := c.Writer.Status()
log.Println(status)
}
}
func main() {
r := gin.New()
r.Use(Logger()) // 使用自定义中间件
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
log.Println(example)
})
// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}
2.7 路由匹配
通过 Engine 的各个方法,创建不同 HTTP 方法的路由。其中第一个参数指定了请求路由,如下面 GET 请求路由将匹配 /someGet。
router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
路由匹配还可以不固定,而是灵活地匹配实际的名称,并在代码中获取路径名称作为变量。
func main() {
router := gin.Default()
// 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 此 handler 将匹配 /user/john/ 和 /user/john/send
// 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.Run(":8080")
}
通过路由组,可以将同样前缀的路由定义在一起,使路由设置更加简洁明了。
func main() {
router := gin.Default()
// 简单的路由组: v1
v1 := router.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// 简单的路由组: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
2.8 重定向
外部重定向:
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
内部进行 HTTP 重定向:
r.POST("/test", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/foo")
})
路由重定向:
r.GET("/test", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})
2.9 静态文件服务
对于路由指向特定目录或文件。
func main() {
router := gin.Default()
router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")
// 监听并在 0.0.0.0:8080 上启动服务
router.Run(":8080")
}
3. 开发文档
3.1 常量
常见的 Content-Type 和 MIME 类型。
const (
MIMEJSON = binding.MIMEJSON
MIMEHTML = binding.MIMEHTML
MIMEXML = binding.MIMEXML
MIMEXML2 = binding.MIMEXML2
MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
MIMETOML = binding.MIMETOML
)
模式。
const (
// DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release"
// TestMode indicates gin mode is test.
TestMode = "test"
)
3.2 函数
// DisableBindValidation 关闭默认的验证器
func DisableBindValidation()
// DisableConsoleColor 关闭终端输出的颜色
func DisableConsoleColor()
// ForceConsoleColor 终端输出颜色
func ForceConsoleColor()
// Mode 返回当前的模式
func Mode() string
// SetMode 设置模式
func SetMode(value string)
3.3 Context
Content 可以在 middleware 之间传递变量,检验请求 JSON 的格式,生成返回的 JSON。
type Context struct {
Request *http.Request
Writer ResponseWriter
Params Params
Keys map[string]any // 保存KV数据,用于上下文间的数据共享
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// contains filtered or unexported fields
handlers HandlersChain // 从路由树取出来的中间件 handlers
index int8 // 执行到的 handlers 索引位置
fullPath string
engine *Engine
params *Params
skippedNodes *[]skippedNode
}
方法:
// Bind 将请求体绑定到结构体
func (c *Context) Bind(obj any) error
func (c *Context) BindHeader(obj any) error
func (c *Context) BindJSON(obj any) error
func (c *Context) BindQuery(obj any) error
func (c *Context) BindUri(obj any) error
func (c *Context) BindWith(obj any, b binding.Binding) error
func (c *Context) MustBindWith(obj any, b binding.Binding) error
func (c *Context) ShouldBind(obj any) error
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error
// Abort 防止等待中的 handler 被调用,但不会停止它
func (c *Context) Abort()
func (c *Context) AbortWithError(code int, err error) *Error
func (c *Context) AbortWithStatus(code int)
func (c *Context) AbortWithStatusJSON(code int, jsonObj any)
// ClientIP 返回客户端的IP
func (c *Context) ClientIP() string
// ContentType 获取content-type
func (c *Context) ContentType() string
// Cookie 获取cookie
func (c *Context) Cookie(name string) (string, error)
// SetCookie 设置cookie
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
// FullPath 请求路径匹配的路由
func (c *Context) FullPath() string
// Param 获取url参数
func (c *Context) Param(key string) string
// Get 根据key获取value
func (c *Context) Get(key string) (value any, exists bool)
// Set 设置key/value
func (c *Context) Set(key string, value any)
// GetPostForm 从POST请求体获取key的value
func (c *Context) GetPostForm(key string) (string, bool)
func (c *Context) PostForm(key string) (value string)
// GetQuery 从url参数获取key的value
func (c *Context) GetQuery(key string) (string, bool)
func (c *Context) Query(key string) (value string)
// GetRawData 获取流数据
func (c *Context) GetRawData() ([]byte, error)
// Data 将数据写入返回体
func (c *Context) Data(code int, contentType string, data []byte)
func (c *Context) String(code int, format string, values ...any)
func (c *Context) TOML(code int, obj any)
func (c *Context) XML(code int, obj any)
func (c *Context) YAML(code int, obj any)
// Error 将error放到当前context
func (c *Context) Error(err error) *Error
// Status 设置HTTP返回码
func (c *Context) Status(code int)
// HTML 根据模板返回响应内容
func (c *Context) HTML(code int, name string, obj any)
// JSON 将响应序列化
func (c *Context) JSON(code int, obj any)
// Header 获取响应的头
func (c *Context) Header(key, value string)
// Handler 返回handler
func (c *Context) Handler() HandlerFunc
func (c *Context) HandlerName() string
func (c *Context) HandlerNames() []string
// Next 在middleware中调用,执行中间件链中下一个
func (c *Context) Next()
// Redirect 重定向
func (c *Context) Redirect(code int, location string)
3.4 Engine
Engine 是框架的实例,包含了中间件、配置。
type Engine struct {
RouterGroup
RemoteIPHeaders []string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
// ...
// contains filtered or unexported fields
pool sync.Pool // gin.Context 缓存池
trees methodTrees // 不同 HTTP 方法的路由树
}
新建对象:
// Default 包含默认的 Logger 和 Recovery 中间件
func Default() *Engine
// New 无中间件
func New() *Engine
方法:
// Run 监听端口并启动HTTP服务
func (engine *Engine) Run(addr ...string) (err error)
// Routes 获取已绑定的routes
func (engine *Engine) Routes() (routes RoutesInfo)
// Use 绑定中间件
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes
3.5 Handler
HandlerFunc 定义了以 gin.Context 为参数的函数,用于中间件返回。
type HandlerFunc func(*Context)
一些 Gin 实现的中间件:
// BasicAuth 基础的 HTTP 验证
func BasicAuth(accounts Accounts) HandlerFunc
// Bind 根据参数返回中间件
func Bind(val any) HandlerFunc
// Logger 打印日志
func Logger() HandlerFunc
// CustomRecovery 从 panic 中恢复
func Recovery() HandlerFunc
func CustomRecovery(handle RecoveryFunc) HandlerFunc
HandlersChain 是 HandlerFunc 的切片。
type HandlersChain []HandlerFunc
方法:
// Last 返回最后一个handler,也就是主handler
func (c HandlersChain) Last() HandlerFunc
RecoveryFunc 定义了 CustomRecovery 用到的参数。
type RecoveryFunc func(c *Context, err any)
3.6 Route
IRouter 定义了所有路由,包括单个路由和路由组。
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
IRoutes 定义了路由。
type IRoutes interface {
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
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
Match([]string, string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
StaticFileFS(string, string, http.FileSystem) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
RouteInfo 表示路由的详细内容。
type RouteInfo struct {
Method string
Path string
Handler string
HandlerFunc HandlerFunc
}
RoutesInfo 表示 RouteInfo 的切片。
type RoutesInfo []RouteInfo
RouterGroup 表示路由组,包含前缀以及一系列的中间件。
type RouterGroup struct {
Handlers HandlersChain
// contains filtered or unexported fields
}
方法:
// BasePath 返回路由组的基础路径
func (group *RouterGroup) BasePath() string
// Group 创建下层的路由组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
// Use 添加中间件
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes
// Any 注册匹配所有 HTTP 方法的路由
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes
// 注册不同的 HTTP 方法
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
3.7 Param
Param 表示一个 URL 参数。
type Param struct {
Key string
Value string
}
Params 表示 Param 的切片。
type Params []Param
方法:
// 获取参数对应的value
func (ps Params) ByName(name string) (va string)
func (ps Params) Get(name string) (string, bool)
3.8 ResponseWriter
ResponseWriter 表示写入响应的接口。
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
// Status HTTP 返回码
Status() int
// Size 已写入的字节数
Size() int
// WriteString 写入响应体
WriteString(string) (int, error)
// Written 是否已写入响应体
Written() bool
// WriteHeaderNow 写入响应头
WriteHeaderNow()
// Pusher 获取 http.Pusher
Pusher() http.Pusher
}
4. 原理分析
4.1 流程与数据结构
Gin 是在 Go 的标准库 net/http 的基础之上进行封装而成,一个基础的服务启动包括了调用 gin.Default() 创建 gin.Engine 对象,注册路由,以及 Engine.Run() 启动 HTTP 服务监听端口三部分。
它们之间的交互流程如下图:
其中设计的数据结构主要有用于缓存 gin.Context 对象的 sync.Pool,储存路由组的 RouterGroup,每个 HTTP 方法对应的路由树 gin.methodTrees。
4.2 服务启动
首先需要调用 gin.Default() 创建一个 Engine 对象,将会创建一个路由树,然后创建一个 gin.Context 池用于后面的使用。
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认中间件
return engine
}
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{ // 初始化路由树
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine
engine.pool.New = func() any { // gin.Context 池
return engine.allocateContext(engine.maxParams)
}
return engine
}
然后创建路由,将对应的函数加到路由树上。
这里以 POST 请求的路由添加为例:
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) // 绝对路径
handlers = group.combineHandlers(handlers) // 将路由组的 handlers 加上参数传入的 handlers 作为该请求的 handlers,最后一个是该请求的业务处理函数
group.engine.addRoute(httpMethod, absolutePath, handlers) // 将 HTTP 方法、绝对路径、handlers 加入路由树
return group.returnObj()
}
这里将请求添加到路由树里,每个 HTTP 方法对应一个路由树。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) // 每个 HTTP 方法对应一个路由树
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
if paramsCount := countParams(path); paramsCount > engine.maxParams { // 获取路由规则里的变量
engine.maxParams = paramsCount
}
if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
engine.maxSections = sectionsCount
}
}
然后调用 Engine.Run() 启动服务,其内部实际上是调用 http.ListenAndServe() 来监听端口,然后调用 accept() 和 serve() 函数来监听并服务接口调用。
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address) // 端口,默认 8080
err = http.ListenAndServe(address, engine.Handler()) // 启动 http web server 监听端口
return
}
http.ListenAndServe 的第二个参数是 http.Handler 接口,需要实现 ServeHTTP() 方法,而 gin.Engine 则实现了该方法。在这里 Gin 使用了 sync.Pool 来存储和复用 gin.Context 对象,降低了性能损耗。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context) // 从 gin.Context 池获取对象
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c) // 处理 http 请求
engine.pool.Put(c) // 将对象放回池子里
}
4.3 中间件
路由组结构体定义如下:
type RouterGroup struct {
Handlers HandlersChain // 中间件 handler 切片
basePath string // 基础路径
engine *Engine // Engine 对象的指针
root bool // 当前路由组对象是否为根路由
}
初始化 gin.Engine 对象时,创建了一个 Handlers 为 nil,basePath 为 “/",root 为 true 的路由组对象。
而在根路由组对象中继续创建子路由组,basePath 等于父路由组的加上相对路径,如果调用 Group() 方法传入了 handlers 也就是中间件函数的 handler,则追加到后面。
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers), // 父路由组的 handlers 追加 handlers
basePath: group.calculateAbsolutePath(relativePath), // 将相对路径追加到 basePath
engine: group.engine,
}
}
注册中间件时,如果在 Engine 对象注册中间件,则注册到根路由组。如果是在 RouterGroup 对象直接通过调用 Use 方法,则直接讲中间件注册到当前对象的 Handlers。如果是注册请求时带了中间件,则将路由组的 handlers 加上参数传入的 handlers 作为该请求的 handlers,再将 HTTP 方法、绝对路径、handlers 加入路由树。
// Engine 对象注册中间件
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 注册到根路由组
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
// 路由组注册中间件
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...) // 追加到 Handlers
return group.returnObj()
}
// 注册请求
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) // 绝对路径
handlers = group.combineHandlers(handlers) // 将路由组的 handlers 加上参数传入的 handlers 作为该请求的 handlers,最后一个是该请求的业务处理函数
group.engine.addRoute(httpMethod, absolutePath, handlers) // 将 HTTP 方法、绝对路径、handlers 加入路由树
return group.returnObj()
}
注册请求时,路由组的 handlers 和请求的 handler 合起来,放入路由树。
执行一个 HTTP 请求时调用 Engine.handleHTTPRequest。
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// 根据 HTTP 方法,从路由树切片中找到对应的路由树
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 根据路径从路由树找到节点
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers // 将中间件 handlers 设置到 gin.Context 中
c.fullPath = value.fullPath
c.Next() // 依次执行 handlers 的中间件函数
c.writermem.WriteHeaderNow()
return
}
break
}
// 找不到路由,404
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
主要流程步骤包含:
- 根据 HTTP 方法,从路由树切片中找到对应的路由树
- 根据路径从路由树找到节点
- 将中间件 handlers 设置到 gin.Context 中
- 依次执行 handlers 的中间件函数
上面找到对应路由树的节点后,获取了该路由请求所添加的中间件 handlers,调用 c.Next() 方法开始依次遍历调用中间件函数,handlers 中最后一个是该请求的业务处理函数。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c) // 执行中间件 handlers 函数
c.index++ // 索引指向下一位置
}
}
在一个中间件函数中,可以调用 c.Next(),这样前面的代码段就在执行请求前执行,后面的代码段就在执行完请求后执行。且多个中间件函数之间存在嵌套关系,执行请求前依次执行中间件函数的前半段,执行完请求后就会反序依次执行中间件函数的后半段。
func handler1() gin.HandlerFunc {
return func(c *gin.Context) {
// prev process
c.Next()
// post process
}
}
func handler2() gin.HandlerFunc {
return func(c *gin.Context) {
// prev process
c.Next()
// post process
}
}
func handler3() gin.HandlerFunc {
return func(c *gin.Context) {
// prev process
c.Next()
// post process
}
}
Gin 限制了 handlers 索引到达 63 时会 panic,也就是一个请求最多只支持 62 个中间件。
用户可以在某个 handler 中通过调用 Context.Abort 方法实现 handlers 链路的提前熔断。
4.4 路由树
路由树存储在 Engine.trees 成员中,每个 HTTP 方法有一个自己的路由树,注册请求时会遍历 Engine.trees 找到对应的那个路由树进行注册。
// 不同 HTTP 方法的路由树
type methodTrees []methodTree
// 路由树
type methodTree struct {
method string // HTTP 方法
root *node // 根节点
}
// 节点
type node struct {
path string // 当前一层的路由路径名称
indices string
wildChild bool
nType nodeType
priority uint32
children []*node // 子结点
handlers HandlersChain // 中间件切片
fullPath string // 完整路径
}
前缀树(trie 树)是一种树形数据结构,是一种基于字符串公共前缀构建索引的树状结构,它的特点是:
- 除根节点之外,每个节点对应一个字符
- 从根节点到某一节点,路径上经过的字符串联起来,即为该节点对应的字符串
- 尽可能复用公共前缀,如无必要不分配新的节点
而 Gin 使用了压缩前缀树(radix 树),它在前缀树的基础上进行了优化,倘若某个子节点是其父节点的唯一孩子,则与父节点进行合并,减少了存储和查询的损耗。
如图从左边几个已注册的路由,生成了右边的压缩前缀树。