本文主要对GIN框架的两个核心概念(router和context)进行代码解读。
Router
1、支持POST,GET,PUT等多种方法,实现如下:
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
2、使用前缀树实现的router树,结点(node)的实现如下:
type node struct {
path string // uri的路径
indices string // 子节点的首字母集合
wildChild bool // 是否是通配字段
nType nodeType // 结点类型
priority uint32 // 优先级
children []*node // 子结点
handlers HandlersChain // 绑定的处理函数
fullPath string
}
这里有一处实现巧妙的地方。作者通过priority来实现子节点的优先级,优先级越高的子节点indices字段中出现的位置越靠前,实现如下:
func (n *node) addChild(child *node) {
if n.wildChild && len(n.children) > 0 {
wildcardChild := n.children[len(n.children)-1]
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
} else {
n.children = append(n.children, child)
}
}
优先级的计算方式为出现一个相同path首字母的子结点,优先级加一。我理解作者是基于这样的假定,如果注册的path越集中,则请求来的path更大概率会出现在这些集中的node中。
3、在前缀树的基础上,实现两种通配字段,使用起来非常的方便,举例如下:
3.1 通配符’:’: 能匹配两个‘/’中的字符串
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
它能匹配/user/john,但不能匹配/user/ 或 /user 或 /user/john/smith
3.2 通配符’*’: 能匹配‘/’之后所有的字符串,它之后不允许再出现别的通配符
router.GET("/user/*name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
它能匹配/user/john 或 /user/john/smith 或 /user/,但不能匹配/user/ 或 /user 或 /user/john/smith
使用通配字段可以实现更加简洁的uri,比如/user/:id,可以直接将用户的id写在path里,而不必向通常那样写成这样 /user?id=xxx
代码实现在addRoute和getValue函数中,比较晦涩(细节很多),可以找一些case自己走一遍,这样能更快的理解代码。另外作者写了很多单元测试用例,通过这些用例也可以更容易理解函数的功能。
Context
type Context struct {
writermem responseWriter // 读写请求
Request *http.Request
Writer ResponseWriter
Params Params // 请求的参数
handlers HandlersChain // 处理函数链
index int8 // 当前正在处理函数的序号
fullPath string
engine *Engine
params *Params // 请求的query参数
mu sync.RWMutex
Keys map[string]interface{} // 公共参数,middleware可以设置
Errors errorMsgs // 错误消息组
Accepted []string // 接受请求格式类型
queryCache url.Values // 请求参数缓存
formCache url.Values // 请求体缓存
sameSite http.SameSite
}
1、index
该值表示处理到HandlersChain的第几个函数。正常情况下是顺序进行处理,当处理完成后,index值加1,实现如下:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
如果出现错误的情况,前面的处理函数(或者叫中间件)可以中断本次请求,直接返回错误信息,实现如下:
func (c *Context) Abort() {
c.index = abortIndex
}
可见是通过index的值来控制服务继续处理还是提前终止的。
这样的实现方式允许调用者灵活的编写处理函数,如下:
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
signature += "B"这一语句会在所有处理函数调用完成后再执行。
2、Keys
所有处理函数均可读写该变量,后面的处理函数可以读取前面处理函数写的值。
实现如下:
func (c *Context) Set(key string, value interface{}) {
c.mu.Lock()
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
c.Keys[key] = value
c.mu.Unlock()
}
func (c *Context) Get(key string) (value interface{}, exists bool) {
c.mu.RLock()
value, exists = c.Keys[key]
c.mu.RUnlock()
return
}
比如我们有一个中间件专门来验证用户信息,当核验成功后,会把用户的一些基本信息写入keys中,这样后面的处理函数就可以直接拿到这些信息了。
3. Binding
框架支持直接处理json、xml等格式的请求体。并且对其字段进行验证(如未识别的字段名),如下:
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
binding:"required"`表示这个字段是必须的,否则会报错(直接)。它的申明表明同时支持json和xml两种格式,gin框架支持通过content type推断请求的格式体。
另一个例子如下:
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
它指定的时间字段的格式,同时要求check_out字段的时间一定在check_in字段之后。
默认使用https://github.com/go-playground/validator/tree/v8.18.2 进行请求验证,Gin框架同时支持自定义的验证函数,如下:
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
if today.After(date) {
return false
}
}
return true
}
4. Render Binding是gin框架对于用户请求解析的支持,render则是对于返回结果的支持。首先同样支持多类型的结果返回,其次能完成status code和content type的填写,最后能由不同的返回格式序列化返回结果。