上次我们说到 [gin 的启动过程及实现](),今天来细讲 gin 的路由。
用法
还是老样子,先从使用方式开始:
func main() {
r := gin.Default()
r.GET("/hello", func(context *gin.Context) {
fmt.Fprint(context.Writer, "hello world")
})
r.POST("/somePost", func(context *gin.Context) {
context.String(http.StatusOK, "some post")
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
平时开发中,用得比较多的就是 Get
和 Post
的方法,上面简单的写了个 demo,注册了两个路由及处理器,接下来跟着我一起一探究竟
注册路由
从官方文档和其他大牛的文章中可以知道,gin
的路由是借鉴了 httprouter
实现的路由算法,所以得知 gin
的路由算法是基于前缀树
这个数据结构的。
从 Get
方法进去看源码:
r.GET("/hello", func(context *gin.Context) {
fmt.Fprint(context.Writer, "hello world")
})
会来到 routergroup.go
的 Get
函数,可以发现方法的承载者已经是 *RouterGroup
:
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}
从注释中我们可以看到 GET is a shortcut for router.Handle("GET", path, handle)
也就是说 GET
方法的注册也可以等价于:
helloHandler := func(context *gin.Context) {
fmt.Fprint(context.Writer, "hello world")
}
r.Handle("GET", "/hello", helloHandler)
再来看一下 Handle
方法的具体实现:
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
panic("http method " + httpMethod + " is not valid")
}
return group.handle(httpMethod, relativePath, handlers)
}
不难发现,无论是 r.GET
还是 r.Handle
最终都是指向了 group.handle
:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 计算绝对路径,这是因为可能会有路由组会在外层包裹的原因
absolutePath := group.calculateAbsolutePath(relativePath)
// 联合路由组的 handler 和新注册的 handler
handlers = group.combineHandlers(handlers)
// 注册路由的真正入口
group.engine.addRoute(httpMethod, absolutePath, handlers)
// 返回 IRouter 接口对象,这个放在路由组进行分析
return group.returnObj()
}
接下来又回到了 gin.go
,可以看到上面的注册入口是通过group.engine
调用的,大家不用看 routerGroup
的结构也大致猜出来了吧,其实 engine
才是真正的路由树 router
,而 gin
为了实现路由组的功能,所以在外面又包了一层 routerGroup
,实现路由分组,路由路径组合隔离的功能。
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)
// 每个httpMethod都拥有自己的一颗树
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 在路由树中添加路径及请求处理handler
root.addRoute(path, handlers)
}
以上就是注册路由的过程,整体流程其实挺清晰的。
路由树
终于来到了关键的实现路由树的地方tree.go
:
先来看看 tree
的结构:
type methodTree struct {
method string
root *node
}
type methodTrees []methodTree
上面的 engine.trees.get(method)
就是遍历这个以 httpMethod
分隔的数组:
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
关键在于 node
:
type node struct {
path string // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径)
indices string // 所有孩子节点的path[0]组成的字符串
children []*node // 孩子节点
handlers HandlersChain // 当前节点的处理函数(包括中间件)
priority uint32 // 当前节点及子孙节点的实际路由数量
nType nodeType // 节点类型
maxParams uint8 // 子孙节点的最大参数数量
wildChild bool // 孩子节点是否有通配符(wildcard)
fullPath string // 路由全路径
}
nType
有这几个值:
const (
static nodeType = iota // 普通节点,默认
root // 根节点
param // 参数路由,比如 /user/:id
catchAll // 匹配所有内容的路由,比如 /article/*key
)
下面的 addRoute
方法就是对这棵前缀树的构建过程,实际上就是不断寻找最长前缀的过程。
func (n *node) addRoute(path string, handlers HandlersChain) {
……
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk:
……
// Make new node a child of this node
if i < len(path) {
……
c := path[0]
// 一系列的判断与校验
……
// Otherwise insert it
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
// 经过重重困难,终于可以摇到号了
n.insertChild(numParams, path, fullPath, handlers)
return
} else if i == len(path) { // Make node a (in-path) leaf
// 路由重复注册
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
}
return
}
} else { // Empty tree
// 空树则直接插入新节点
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
}
}
最后画一下 gin
构建前缀树的示意图:
r.GET("/", func(context *gin.Context) {})
r.GET("/test", func(context *gin.Context) {})
r.GET("/te/n", func(context *gin.Context) {})
r.GET("/pass", func(context *gin.Context) {})
r.GET("/part/:id", func(context *gin.Context) {})
r.GET("/part/:id/pen", func(context *gin.Context) {})
动态路由
在画前缀树的时候,写到一个了路由 /part/:id
,这里的 :id
就是动态路由了,可以根据路由中指定的参数来解析 url 中对应动态路由里的参数值。
其实在说到 node
的数据结构的时候,已经提到了 nType
、maxParams
、wildChild
这三个字段与动态路由的设计实现有关的,下面就是关于路由注册时如果是动态路由时的处理:
// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
……
if c == ':' { // param
// 在通配符开头拆分路径
if i > 0 {
n.path = path[offset:i]
offset = i
}
child := &node{
nType: param,
maxParams: numParams,
fullPath: fullPath,
}
n.children = []*node{child}
// 如果孩子节点是参数路由,就会将本节点wildChild设置为true
n.wildChild = true
n = child
n.priority++
numParams--
// 如果路径没有以通配符结尾,则将有另一个以"/" 开头的非通配符子路径
// 可以理解为后面还有节点
if end < max {
n.path = path[offset:end]
offset = end
child := &node{
maxParams: numParams,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n = child
}
} else { // catchAll
……
n.path = path[offset:i]
// 匹配所有内容的通配符 如 /*key
// first node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
fullPath: fullPath,
}
n.children = []*node{child}
n.indices = string(path[i])
// 在这里将 node 进行赋值了
n = child
n.priority++
// second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handlers: handlers,
priority: 1,
fullPath: fullPath,
}
n.children = []*node{child}
return
}
}
// insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handlers = handlers
n.fullPath = fullPath
}
我们知道 gin
框架中对于动态路由参数接收时是用 context.Param(key string)
的,下面跟着一个简单的 demo 来做
helloHandler := func(context *gin.Context) {
name := context.Param("name")
fmt.Fprint(context.Writer, name)
}
r.Handle("GET", "/hello/:name", helloHandler)
来看下 Param
写了啥:
// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
// router.GET("/user/:id", func(c *gin.Context) {
// // a GET request to /user/john
// id := c.Param("id") // id == "john"
// })
func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
看注释,其实写得已经很明白了,这个函数会返回动态路由中关于参数在请求 url
里的值,再往深处走,Params
和 ByName
其实来自 tree.go
:
// context.go
type Context struct {
……
Params Params
……
}
// tree.go
type Param struct {
Key string
Value string
}
// Params 是有个有序的 Param 切片,路由中的第一个参数会对应切片的第一个索引
type Params []Param
// 遍历 Params 获取值
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
// 封装了一下,调用上面的 Get 方法
func (ps Params) ByName(name string) (va string) {
va, _ = ps.Get(name)
return
}
获取参数 key
的地方找到了,那从路由里拆解并设置 Params
的地方呢?
// tree.go
type nodeValue struct {
handlers HandlersChain
params Params
tsr bool
fullPath string
}
// getValue 返回的 nodeValue 的结构,里面包含处理好的 Params
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
value.params = po
walk: // Outer loop for walking the tree
for {
if len(path) > len(n.path) {
if path[:len(n.path)] == n.path {
path = path[len(n.path):]
// 如果这个节点没有通配符,就进行往孩子节点遍历
if !n.wildChild {
c := path[0]
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
n = n.children[i]
continue walk
}
}
// 如果没找到有通配符标识的节点,直接重定向到该 url
value.tsr = path == "/" && n.handlers != nil
return
}
// handle wildcard child
n = n.children[0]
switch n.nType {
//可以看到这里是用 nType 来判断的
case param:
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// 遍历 url 获取参数对应的值
// save param value
if cap(value.params) < int(n.maxParams) {
value.params = make(Params, 0, n.maxParams)
}
i := len(value.params)
value.params = value.params[:i+1] // expand slice within preallocated capacity
value.params[i].Key = n.path[1:] // 除去 ":",如 :id -> id
val := path[:end]
// url 编码解析以及 params 赋值
if unescape {
var err error
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
value.params[i].Value = val // fallback, in case of error
}
} else {
value.params[i].Value = val
}
……
}
}
}
讲到这里就已经对路由注册和动态路由的实现流程和原理分析得差不多了,画一个核心流程图总结一下:
路由组
gin
用 RouterGroup
路由组包住了路由实现了路由分组功能。之前说到 engine
的时候说到 engine 的结构中是组合了 RouterGroup
的,而 RouterGroup
中其实也包含了 engine
:
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
type Engine struct {
RouterGroup
...
}
这样的做法让 engine
直接拥有了管理路由的能力,也就是 engine.GET(xxx)
可以直接注册路由的来由。而 RouterGroup
中包含了 engine
的指针,这样实现了 engine
的单例,这个也是比较巧妙的做法之一。
不仅如此,RouterGroup
实现了 IRouter
接口,接口中的方法都是通过调用 engine.addRoute()` 将handler链接到路由树中:
var _ IRouter = &RouterGroup{}
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
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
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
路由组的功能显而易见,就是让路由分组管理,在组内的路由的前缀都统一加上组路由的路径,看下 demo
:
router := gin.Default()
v1 := router.Group("/v1")
{
v1.POST("/hello", helloworld) // /v1/hello
v1.POST("/hello2", helloworld2) // /v1/hello2
}
包住路由并在注册路由时进行拼接的地方是在注册路由的函数中:
// routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 拼接获取绝对路径
absolutePath := group.calculateAbsolutePath(relativePath)
// 合并路由处理器集合
handlers = group.combineHandlers(handlers)
……
}
参考链接:
1)https://segmentfault.com/a/11...
2)https://blog.csdn.net/u013949...
有疑问加站长微信联系(非本文作者)