孤独是一个陪伴人一生的伙伴,是一个既定事实,与其否认,与其抗争,与其无谓的逃避,不如接受它,拥挤的人群里让它保护你回家,周六的上午让它陪你吃早餐,整理阳光。
目标
Go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
这是官方文档中 Running Gin 的栗子,我们也从这里开始看
gin.Default()
可以看到一开始就会调用 Default
方法,所以我们也从此处入手,看到源码中的 Default
函数
Go
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
可以看到 Default
做了以下事情:
- 返回值一个
Engine
实例 - 调用
debugPrintWARNINGDefault
方法 - 创建
Engine
实例 engine
调用Use
方法,参数为两个函数的返回值(Logger,Recovery)
Engine 是什么?
Engine
是一个结构体,其中定义了很多属性。
Go
type Engine struct {
RouterGroup
RedirectTrailingSlash bool
RedirectFixedPath bool
HandleMethodNotAllowed bool
ForwardedByClientIP bool
AppEngine bool
UseRawPath bool
UnescapePathValues bool
RemoveExtraSlash bool
RemoteIPHeaders []string
TrustedPlatform string
MaxMultipartMemory int64
UseH2C bool
ContextWithFallback bool
delims render.Delims
secureJSONPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
}
什么?这也太多了,眼花缭乱,暂时先不用管,用到哪个再看哪个
既然这样,New
方法无非就是返回了这个了,赋了下初始值咯。
创建 Engine 实例
Go
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 {
return engine.allocateContext(engine.maxParams)
}
return engine
}
Use() 做了什么?
找到 Engine
的方法 Use
,先看它里面写的什么吧
Go
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
从字面意思就可以看出它的作用,就是添加中间件,这个函数的核心应该就是 engine.RouterGroup.Use(middleware...)
这串代码了吧,那在看看它究竟做了什么吧。
进入 RouterGroup.Use()
首先,从上面 struct Engine{}
中就可以看出 RouterGroup
也是一个结构体
Go
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
func (group *RouterGroup) returnObj() IRoutes {
if group.root {
return group.engine
}
return group
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
看到这个 Use
方法,好像明白了它作用了,原来就是把 middleware
添加进 group.Handlers
中,那么再继续查看 HandlersChain
是什么
Go
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
它就是一个元素为 HandlerFunc
的 slice
,所以反向结论,中间件也是一个 HandlerFunc
到现在我们回顾一下,我们目前做完操作,最后得到的结果是什么
Go
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: [Logger(), Recovery()],
basePath: "/",
root: true,
},
...
}
最后 engine
相当于 Handlers
添加了两个函数,至此 Default
函数完。
r.GET()
GET
方法在里面又做了什么呢,通过 Goland ,按住 ctrl + 点击
,跳转到源码可以看到GET
方法主要是为了调用一个 handle
的方法
Go
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
所以搞清楚 handle
做了什么,就知道 GET
做了什么
Go
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()
}
从字面意思可以理解为:
group.calculateAbsolutePath(relativePath)
可能是将相对路径转化成绝对路径group.combineHandlers(handlers)
看样子可能是处理存入 handlers 的中间件group.engine.addRoute(httpMethod, absolutePath, handlers)
将请求方法,处理后的路径,和要执行的handlers
不知道是怎么处理一下,搞成了路由
猜想完毕,带着猜想进入函数内部一探究竟!
calculateAbsolutePath()
Go
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
可以看到,这个函数内部也只是调用了函数 joinPaths
,继续深入:
Go
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
func lastChar(str string) uint8 {
if str == "" {
panic("The length of the string can't be 0")
}
return str[len(str)-1]
}
这样很明显可以看出主要目的就是将 group.basePath
和 relativePath
合并,relativePath
我们知道是自己在方法中传递的值,那 group.basePath
呢? 回到 New
方法中查看了下,在初始化生成的时候,basePath: "/"
,absolutePath
的值为 /ping
combineHandlers()
Go
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
这个函数主要功能就是将 中间件
和对应的处理函数,放在同一个 slice
中,可能为了后面的执行顺序,这里 handlers
的值就变成了 [Logger(), Recovery(), handler()]
addRoute()
addRoute
应该可以说是这里最重要的逻辑处理了吧,看看它内部做了什么吧
Go
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
...other code
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
...other code
}
首先,看着这段代码,来猜测一下,它的意思以及作用:
root := engine.trees.get(method)
,可能是为了从trees
中获取对应method
的根节点engine.trees = append(engine.trees, methodTree{method: method, root: root})
不存在根节点,那就创建根节点root.addRoute(path, handlers)
存在根节点那就直接在根节点中添加
engine.trees.get()
进入到 get
方法中查看
Go
type methodTrees []methodTree
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
它会遍历 engine.trees
,找出为 GET
方法创建好的节点并返回
创建节点
当还没有创建这个方法的根节点时,就会新建 methodTree
Go
methodTree{method: method, root: root}
root.addRoute()
这个函数执行就是整体路由的核心了,所以必须要知道它在做什么(这个函数实现有点复杂,分两部分看)
Go
// 第一部分
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
// 第二部分
...other code
}
我们上面已知 path
和 handlers
的值,从这第一部分可以看出,也就是创建根节点的时候的操作。接下来继续看第二部分
Go
// 第二部分
walk:
for {
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
i := longestCommonPrefix(path, n.path)
// Split edge
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handlers: n.handlers,
priority: n.priority - 1,
fullPath: n.fullPath,
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// Make new node a child of this node
if i < len(path) {
path = path[i:]
c := path[0]
// '/' after param
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
if c != ':' && c != '*' && n.nType != catchAll {
// []byte for proper unicode char conversion, see #65
n.indices += bytesconv.BytesToString([]byte{c})
child := &node{
fullPath: fullPath,
}
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
} else if n.wildChild {
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
n = n.children[len(n.children)-1]
n.priority++
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
}
// Wildcard conflict
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
n.insertChild(path, fullPath, handlers)
return
}
// Otherwise add handle to current node
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
n.fullPath = fullPath
return
}
代码太长就不一一列举了,这个里面总共做了:
- 查找最长公共前缀,就是为了找到当前路径与已经存在的路由节点路劲的最长共同部分,便于是否需要将节点进行分割
- 分割边,就是如果最长的公共前缀的长度小于节点路径
n.path
的长度,说明当前路径与已经存在的部分节点路由有重合部分,但还有剩余部分不同,需要进行分割,并创建新节点表示剩余部分 - 处理参数和通配符,就是最长公共前缀大于的长度与
n.path
相同,且当前路径还有剩余部分,则根据当前路径的第一个字符判断如何处理参数(例如::id, *filepath),如果当前路径是参数或通配符类型,则需要将新节点插入到已存在的参数或通配符节点的子节点列表中。人话:["/", "a", ":id"],与 ["/", "a", "123"],长度相同,但是不一样 - 插入节点,如果当前路径为普通路径(即不是参数,也不是通配符),就会将新节点插入到当前节点的子节点列表中
- 注册处理函数,根据路径找到对应节点,将处理函数,添加进节点的 handles 中
- 通过 walk 不断循环更新路由树的节点的路径,处理函数等属性,保持路由树的结构完整和正确,这样就实现了路由功能
自此,r.get() 便算完了
r.Run()
最后项目需要 Run
起来,调用Run
函数,那 Run
函数是怎么样 run 起来的呢?
Go
func (engine *Engine) Run(addr ...string) (err error) {
...other code
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
这里只有两句代码核心,那就是 resolveAddress
和 启动http服务
resolveAddress()
Go
func resolveAddress(addr []string) string {
switch len(addr) {
case 0:
if port := os.Getenv("PORT"); port != "" {
debugPrint("Environment variable PORT=\"%s\"", port)
return ":" + port
}
debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
return ":8080"
case 1:
return addr[0]
default:
panic("too many parameters")
}
}
这里不管其它,只看为 0,我们没有传参数,默认为 :8080
端口
http.ListenAndServe()
这里第一个参数,想必都知道了就是端口,第二个参数需要传递一个含有 ServeHTTP
方法的结构体就行,它会被当作一个 HTTP处理器
Go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
这里面主要就是为后续操作绑定上下文
,以及将路由到匹配的处理函数执行,也就是你在写 handler
函数时,使用到的 context
Go
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
这样一个简单的例子,所走过的流程,就全部完成了(其中还有些函数没有拿出来看,知道作用就好)
总结
- 带有
ServeHTTP
方法的结构体,就可以被当作成一个HTTP处理器
,也就是每个请求都会通过ServeHTTP
- 对路由的处理是生成了一个路由前缀树,通过给对应树中节点,添加处理函数,达到路由和函数对应的关系,当请求进来的时候,会查询路由树,然后找到对应节点,执行对应节点上的处理函数
- 分组和设置直接设置路由,抽象概念上,它们是同一种逻辑
- 中间件和
handler
函数也是同一种逻辑,只有执行顺序的不同