20-Gin源码
开发套路
- gin 框架
- mysql + gorm
- vue -> 前后端分离的项目
获取参数
- 各式各样的参数获取方式(query参数、form表单、JSON、path参数)
- 参数校验
业务逻辑处理
- 拆解需求去实现
- logic(service)层 -> dao层
返回响应
跟对方约定好返回响应的格式
{ "code": 0, "msg": "操作成功", "err": "", "data": [ {"title": "吃饭", "status": false}, {"title": "睡觉", "status": true} ], }
Gin参数校验
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
// 结构体对应参数校验
// 注意公开结构体,需要传入给gin包 c.ShouldBind() 校验字段,内部进行反射参数
type Param struct {
//前端忽略
ID int `json:"-"`
//前端拿到key名,可以修改
//以及字段限制 binding:"required" 字段必须填写,该字段校验值是否是零值,所以传入false也有问题,因此得更换校验类型
// 需要查询 gin tag文档
//利用指针类型,去区分,用户传入的是零值,还是未传值
Name *string `json:"name" binding:"required"`
Age *int `json:"age" binding:"required"`
// 要么是指针类型,不传值是nil,可以通过binding的校验规则
Married *bool `json:"married" binding:"required"`
//或者使用sql包提供的字段校验,但是麻烦了点
//Married sql.NullBool `json:"married" binding:"required"`
}
/*
传入
{
"code": 0,
"data": {
"name": "老狗",
"age": 18,
"married": false
},
"msg": "success"
}
*/
func main() {
//启动gin服务
r := gin.Default()
r.POST("/test", ParseParams)
r.Run(":17777")
}
// 解析参数
func ParseParams(c *gin.Context) {
// c.Request > Request *http.Request
//1. 获取参数
var p1 Param
if err := c.ShouldBind(&p1); err != nil {
c.JSON(http.StatusOK, fmt.Sprintf("错误>>:%v", err.Error()))
return
}
//2.校验参数
fmt.Printf("拿到参数:%v\n", p1)
//业务逻辑,模拟
//修改结构体数据
*p1.Age = 999
name2 := "神秘大仙"
//想修改源变量的值,用指针
*p1.Name = name2
*p1.Married = true
//3.返回响应
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": p1,
})
}

Gin源码阅读
- 怎么处理请求
- 怎么注册路由的
- 内部路由的结构/原理
- 中间件的执行流程
gin框架如何接入http包(源码)
问题背景
gin是怎么参与到http请求处理的?
答:gin还是使用net/http去处理请求,以接口实现。

对象池
防止频繁创建对象,用完回收
可以用对象池,复用对象,下一次清除对象属性,再次复用,降低重建损耗。
Gin路由源码解读
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。
gin框架本质是根据用户传入的不同的请求 method,URL,找到对应的func执行
例如用map记录、正则表达式
Radix Tree
基数树(Radix Tree)又称为PAT位树(Patricia Trie or crit bit tree),是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:

Radix Tree可以被认为是一棵简洁版的前缀树。
我们注册路由的过程就是构造前缀树的过程,具有公共前缀的节点也共享一个公共父节点。
假设我们现在注册有以下路由信息:
r := gin.Default() //注册路由时,生成前缀树,数据结构(时间、空间都是最优解,算法)
r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)
那么我们会得到一个GET方法对应的路由树,具体结构如下:
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
上面最右边那一列每个*<数字>表示Handle处理函数的内存地址(一个指针)。从根节点遍历到叶子节点我们就能得到完整的路由表。
例如:blog/:post其中:post只是实际文章名称的占位符(参数)。与hash-maps不同,这种树结构还允许我们使用像:post参数这种动态部分,因为我们实际上是根据路由模式进行匹配,而不仅仅是比较哈希值。
由于URL路径具有层次结构,并且只使用有限的一组字符(字节值),所以很可能有许多常见的前缀。这使我们可以很容易地将路由简化为更小的问题。此外,路由器为每种请求方法管理一棵单独的树。一方面,它比在每个节点中都保存一个method-> handle map更加节省空间,它还使我们甚至可以在开始在前缀树中查找之前大大减少路由问题。
为了获得更好的可伸缩性,每个树级别上的子节点都按Priority(优先级)排序,其中优先级(最左列)就是在子节点(子节点、子子节点等等)中注册的句柄的数量。这样做有两个好处:
- 首先优先匹配被大多数路由路径包含的节点。这样可以让尽可能多的路由快速被定位。
- 类似于成本补偿。最长的路径可以被优先匹配,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先匹配(即每次拿子节点都命中),那么路由匹配所花的时间不一定比短路径的路由长。下面展示了节点(每个
-可以看做一个节点)匹配的路径:从左到右,从上到下。
前缀树动画
大家可以参照动画尝试将以下情形代入上面的代码逻辑,体味整个路由树构造的详细过程:
- 第一次注册路由,例如注册search
- 继续注册一条没有公共前缀的路由,例如blog
- 注册一条与先前注册的路由有公共前缀的路由,例如support

请求方法树
在gin的路由中,每一个HTTP Method(GET、POST、PUT、DELETE…)都对应了一棵 radix tree,我们注册路由的时候会调用下面的addRoute函数:

合并路由组
func main() {
//1.数据库初始化
if err := initDB(); err != nil {
log.Println("Connect mysql failed !!")
panic(err)
}
// 生成table
//db.AutoMigrate(&Todo{})
//2.gin路由
r := gin.Default()
//4.小清单,增删改查
//添加
g := r.Group("/api/v1")
//简单的括起来,美观
{
//例如都是统一前缀,/api/v1/todo
g.POST("/todo", createTodoHandler)
g.PUT("/todo", updateTodoHandler)
g.GET("/todo", getTodoHandler)
//拿到path传入的变量
g.DELETE("/todo/:id", deleteTodoHandler)
}
//3.启动server
r.Run(":17799")
}
gin路由组源码,实现的合并2个切片

1.统计2个切片长度
2.初始化新切片,是2个切片的总长度
切片基础
make([]int,2) // [0 0]
make([]int,0,2) // []
mergedHandlers := make(HandlersChain, finalSize)
//把路由组的handlers 和当前路由的处理函数,合并到一个切片里
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
3.
gin框架中间件详解
gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()。
中间件的注册
gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的r := gin.Default()的Default函数开始看,它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件:

中间件的注册
gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的r := gin.Default()的Default函数开始看,它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}
继续往下查看一下Use()函数的代码:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 实际上还是调用的RouterGroup的Use函数
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
从下方的代码可以看出,注册中间件其实就是将中间件函数追加到group.Handlers中:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers) // 将处理请求的函数与中间件函数结合
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的。
const abortIndex int8 = math.MaxInt8 / 2
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { // 这里有一个最大限制
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain,而它本质上就是一个由HandlerFunc组成的切片:
type HandlersChain []HandlerFunc
中间件的执行
我们在上面路由匹配的时候见过如下逻辑:
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next() // 执行函数链条
c.writermem.WriteHeaderNow()
return
}
其中c.Next()就是很关键的一步,它的代码很简单:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
从上面的代码可以看到,这里通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)。
用户 > url > gin(判断url,method) > 执行handlers

我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3)

中止中间件链条
或者通过调用c.Abort()中断整个调用链条,从当前函数返回。
func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}
源码截图

基本中间件数量
POST api/v1/todo
log > recover > m1 > m2 > todoHandler
源码部分
c.handlers = [log,recover,m1,m2,todoHandler]
c.Set()/c.Get()
c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。
c就像是一根绳子,将该次请求相关的所有的函数都串起来了。

gin源码总结
- gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
- gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助
c.Next()方法实现嵌套调用。 - 借助
c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。