1. 从简单例子分析底层原理
官方提供了一个quick start 例子,我们在此基础上增加使用路由分组和中间件,后续一行一行分析gin核心源码
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
g := gin.Default()
r := g.Group("/my")
r.Use(helloMiddelware(), byeMiddleware())
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
g.Run()
}
func helloMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("hello...")
c.Next()
}
}
func byeMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("bye...")
c.Abort()
}
}
2. Engine实例的创建
我们先看第一行 g := gin.Default()
,进入源码,可以看到是通过New()
方法来创建Engine实例的。
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
我们重点看New()
方法
unc New() *Engine {
// debug模式下的打印,可以忽略不看
debugPrintWARNINGNew()
engine := &Engine{
// 顶端分组路由
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
// 省略部分字段
...
// 最大多媒体内存
MaxMultipartMemory: defaultMultipartMemory,
// 初始化一个容量为9的方法前缀树切片,对应9种请求方法
trees: make(methodTrees, 0, 9),
// 模板渲染分隔符
delims: render.Delims{Left: "{{", Right: "}}"},
// 省略部分字段
...
}
engine.RouterGroup.engine = engine
// 为Engine的sync.Pool对象池指定生成对象的函数
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
从源码中我们可以看到New()
方法主要做以下事情:
- 为Engine实例初始化RouterGroup
- 为Engine实例初始化MaxMultipartMemory
- 为Engine实例初始化一个容量为9的方法前缀树切片trees
- 为Engine实例初始化delims
- 为Engine的sync.Pool对象池指定生成对象的函数
其中engine.allocateContext()
主要是初始化Context实例:
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
3. 路由分组的创建
Engine实例创建完成后,我们来看第二行代码r := g.Group("/my")
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
// 合并HandlerFunc
Handlers: group.combineHandlers(handlers),
// 根据相对路径拼接出路由分组的完整路径
basePath: group.calculateAbsolutePath(relativePath),
// 把原路由的Engine传递给新路由
engine: group.engine,
}
}
Group()
方法主要做了以下事情:
- 原路由的Handlers和新加入的Handlers合并
- 根据相对路径拼接出新路由分组的完整路径
- 把原路由的Engine传递给新路由
我们可以稍微看看combineHandlers
方法和calculateAbsolutePath
:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
// 新Handlers的总长度
finalSize := len(group.Handlers) + len(handlers)
// 长度大于等于63,则panic
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
// 创建长度为finalSize的HandlerFunc切片(HandlersChain其实是[]HandlerFunc的别名)
mergedHandlers := make(HandlersChain, finalSize)
// 把原路由的HandlerFunc和新加入的HandlerFunc拷贝到新切片
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
// 根据原路由的路径和新的路径进行拼接
return joinPaths(group.basePath, relativePath)
}
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
4. 中间件
路由分组创建完成,我们可以使用Use
方法来注册中间件:
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
// 把中间件追加到Handlers切片中
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
实际上Use
方法只做了一件事情,就是把中间件追加到Handlers切片。但值得注意的是,在helloMiddleware
中间件中我们使用了c.Next()
来使得处理链继续流转:
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
// 索引自增
c.index++
// 索引小于该树节点的Handlers就继续遍历
for c.index < int8(len(c.handlers)) {
// 执行HandlerFunc
c.handlers[c.index](c)
// 流转到下一个HandlerFunc
c.index++
}
}
其实从源码可以分析出,就算中间件不使用Next
方法,处理链也是会往下流转的。但是问题来了,多次调用Next
方法不就会执行多个遍历吗?
由于是HandlerFunc是公用同一个Context的,A遍历中执行HandlerFunc导致B遍历执行,假设现在只有两个中间件,在B遍历结束时c.index
是大于 handlers长度的,现在回到了A,A执行代码c.index++
必定大于handlers长度,所以A遍历不会往下执行,c.handlers
也就只会被遍历一次。c.Abort()
用于终止处理链:
func (c *Context) Abort() {
c.index = abortIndex
}
在路由分组创建中我们知道handlers最大长度是62,所以通过设置c.index=63
即可终止handlers的遍历
5. 注册路由
从源码看,其实GET, POST, PUT, DELETE
等方法都是通过group.handle
处理:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 拼接路径
absolutePath := group.calculateAbsolutePath(relativePath)
// 追加Handlers
handlers = group.combineHandlers(handlers)
// 把路由添加到路由树
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// 路径,方法,Handlers的校验
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)
// 如果没有,创建一个
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
// 方法树中添加节点
root.addRoute(path, handlers)
// 省略部分代码
...
}
engine.addRoute
主要做两件事:
- 获取方法树的根节点
- 在方法树中新增节点
6. 处理请求
gin运行服务和处理请求其实都是使用net/http
包的相关方法,只是做了封装而已:
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
// 处理IP地址
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 通过net/http的方法来运行socket服务
err = http.ListenAndServe(address, engine)
return
}
我们知道,ListenAndServe
方法的第二个参数是Handler
接口,也就意味着Engine实现了该接口
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取一个Context对象
c := engine.pool.Get().(*Context)
// 初始化Context的相关字段
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理请求
engine.handleHTTPRequest(c)
// 回收对象
engine.pool.Put(c)
}
这里真正处理请求的是handleHTTPRequest
方法
func (engine *Engine) handleHTTPRequest(c *Context) {
// 省略部分代码
...
// Find root of the tree for the given HTTP method
t := engine.trees
// 查找该请求对应的方法树
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 在方法树中查找路由
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
// 把路由的handlers和完整路径赋值给当前上下文
c.handlers = value.handlers
c.fullPath = value.fullPath
// 遍历handlers处理请求
c.Next()
// 写响应头部
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
// 如果不允许该请求方法处理,则在其他方法树中查找路由进行处理
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
// 没有在树中找到路由,则返回404
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
7. 上下文
gin中的Context虽然实现了golang的context.Context
接口,但是只有Value
方法做了实现,它更多的是用来承载请求的字段和相应的处理方法,例如请求参数的获取和绑定
// Deadline always returns that there is no deadline (ok==false),
// maybe you want to use Request.Context().Deadline() instead.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
return
}
// Done always returns nil (chan which will wait forever),
// if you want to abort your work when the connection was closed
// you should use Request.Context().Done() instead.
func (c *Context) Done() <-chan struct{} {
return nil
}
// Err always returns nil, maybe you want to use Request.Context().Err() instead.
func (c *Context) Err() error {
return nil
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
func (c *Context) Value(key interface{}) interface{} {
if key == 0 {
return c.Request
}
if keyAsString, ok := key.(string); ok {
val, _ := c.Get(keyAsString)
return val
}
return nil
}
需要注意的是Copy
方法,gin不建议在协程中使用Context,如果必须要使用也是使用副本。为什么呢?我们之前也分析到Context对象是从pool中获取的,当请求结束会回收Context,如果此时在另外的协程中使用Context就会导致panic。