目录

gin源码

gin是go开发的一个开源高性能http框架,其主要是把go官方的net/http进行了扩展,前缀树实现了动态路由、支持了中间件、对请求信息进行封装方便用户层使用等。本文基于 gin v1.7.2版本

创建流程

一个Engine实例可以使用New 或者 Default进行创建,唯一区别就是Default默认增加了两个中间件:日志Logger(), panic捕获 Recovery()

初始化会初始化以下内容:

//gin.go

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"},
		TrustedProxies:         []string{"0.0.0.0/0"},
		TrustedPlatform:        defaultPlatform,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory,
		trees:                  make(methodTrees, 0, 9),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
	}

	engine.RouterGroup.engine = engine
    //Context上下文Pool
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}

初始化好后,就可以注册业务的相关api,比如GET、POST等。默认情况下所有的api都是在根分组下,举个例子:

	r := gin.Default()
	//此处的api所在的分组是默认分组,所以请求api的时候直接 /ping即可
	r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})

下面说一下注册api的流程:

//routergroup.go

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	//2. 将相对路径转为绝对路径
	//主要就是 分组的路径+请求的路径,比如:分组为 v1/,请求路径是 hello ,这个请求的全路径就是 v1/hello
	absolutePath := group.calculateAbsolutePath(relativePath)
	//3. 因为gin支持中间件,这里是把组携带的handler和传递过来的请求函数进行组合
	handlers = group.combineHandlers(handlers)
	//4. 将中间件和请求函数的组合放入路由中
	//这样的话,一次api请求,会执行一系列的函数集,达到中间件的效果
	//因为中间件是属于组的,所以一个组下的所有api都支持
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

//1. 对外提供的http方法
//其他POST DELETE PUT 等注册流程都一样
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

初始化好一个Engine,并且注册了api后,就可以运行服务,对外使用了。

// main.go
func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})
	r.Run(":9000")
}

// gin.go
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	err = engine.parseTrustedProxies()
	if err != nil {
		return err
	}

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	//gin实现了ServeHTTP(w http.ResponseWriter, req *http.Request) 
	//所以注册到http服务
	err = http.ListenAndServe(address, engine)
	return
}

因为官方 net/http 提供了接口:ServeHTTP(ResponseWriter, *Request),gin实现了接口:

//gin.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)
}

所以在底层收到http消息后,会回调gin实现的ServeHTTP,这样http消息就可以走gin提供的路由、中间件等逻辑了

请求流程

  1. 发起http请求
  2. 底层回调gin注册函数ServeHTTP
  3. 从sync.pool中获取一个可用Context
  4. 因为是结构体并且sync.pool机制不会主动重置Context,所以手动重置Context
  5. 从前缀树中寻找对应路由
  6. 执行请求对应的函数
  7. 将结果写入响应Response
  8. Context放回sync.pool中
//gin.go

//底层回调gin注册函数`ServeHTTP`
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	//从sync.pool中获取一个可用`Context`
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	//因为是结构体并且sync.pool机制不会主动重置`Context`,所以手动重置`Context`
	c.reset()
	//执行请求
	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

从pool中获取,如果没有会进行创建,创建函数是在初始化的时候注册的。

//gin.go

func New() *Engine {
	//...
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
}

func (engine *Engine) allocateContext() *Context {
	v := make(Params, 0, engine.maxParams)
	return &Context{engine: engine, params: &v}
}

分组与中间件

分组的作用

分组的好处是将其下的所有api进行统一管理,如果没有分组,增加一个通用功能,就需要对每一个api分别添加。比如:对/admin开头的路由进行鉴权,gin中只需要这样做:

	gAdmin:=r.Group("/admin").Use(func(c *gin.Context) {
		//鉴权
	})
	gAdmin.GET("/delUser", func(c *gin.Context) {})
	gAdmin.GET("/addUser", func(c *gin.Context) {})

当用户请求 /admin/delUser/admin/addUser时,会先执行鉴权函数。Use也就是增加中间件的方法。

分组的路由

另外一个路由的添加是由分组地址+api的地址组合而成,初始化Engine的时候会默认有个根组它的basePath/

//gin.go
func New() *Engine {
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		//...
	}
	//...
	return engine
}

如果不创建其他组,使用默认组的话:

func main() {
	r := gin.Default()
	//请求路由为 group.basePath+ `/ping` = http://127.0.0.1/ping
	r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})
}

分组有父子关系,下面这个a分组派生于根分组,所以a分组下的api路由是 /a/xxx,b分组下的api路由是/a/b/xxxc分组由根分组派生,所以c分组下的api路由是 /c

func main() {
	r := gin.Default()
	aGroup := r.Group("/a")
	bGroup := aGroup.Group("/b")

	cGroup := r.Group("/c")
}

路由如此,中间件也会如此,组b下的api包含所有父组的中间件:

//routergroup.go

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		//新的组包含父辈的所有中间件
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

中间件的执行

现在知道了分组和路由的关系,看看中间件是如何执行的。gin在注册一个api的时候,会把组中的中间件函数和api函数放到数组里,增加到路由里:

//routergroup.go

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	//将中间件和api函数组合,中间件在数组前面 api函数在其后
	handlers = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}


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
}

来了一个请求后,找到对应路由下的函数放到Context上下文中,调用Next执行,并且要注意的是所有的中间件所有的函数都用的同一个Context

func (engine *Engine) handleHTTPRequest(c *Context) {
		//..
		value := root.getValue(rPath, c.params, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		//..
}

这里说一下Next执行细节,用个例子来说明:对分组a下所有api请求计时

func Logger(c *gin.Context) {
	//开始计时
	t := time.Now()
	//调用下一个函数
	c.Next()
	//计算用时
	latency := time.Since(t)
	log.Print(latency)
}

func Hello(c *gin.Context){
	fmt.Println("hello")
}

func main() {
	r := gin.Default()
	aGroup := r.Group("/a").Use(Logger)
	aGroup.GET("b", Hello)
	r.Run(":9000")
}
  1. 初始化Context
func (c *Context) reset() {
	c.Writer = &c.writermem
	c.Params = c.Params[:0]
	c.handlers = nil
	//index字段是-1
	c.index = -1
	c.fullPath = ""
	c.Keys = nil
	c.Errors = c.Errors[:0]
	c.Accepted = nil
	c.queryCache = nil
	c.formCache = nil
	*c.params = (*c.params)[:0]
}
  1. 调用函数 Next(index=0)
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}
  1. 执行了Logger函数: 开始计时
  2. Logger内部执行c.Next,再次调用Next函数
  3. 因为index是c中的变量,所以会变成index=1
  4. 所以此时执行了Hello
  5. Hello执行后因为index已经变成2了,所以Next完结
  6. Logger因c.Next()完成,继续执行后续操作:计算时间 打印时间
  7. 完成了整个api调用

路由

上面说的各种流程都没讲一个请求过来,是如何找到具体的执行函数的,这里就是路由的作用了。用map存路由表,索引效率高效,但只支持静态路由。类似/hello/:name 可以匹配到 /hello/wang /hello/zhang的动态路由不支持。gin里使用了前缀树来实现。前缀树就不在这里介绍了

创建路由

下面注册了5个api,从源码看看是如何执行的

func main() {
	r := gin.New()

	r.GET("/index", func(c *gin.Context) {
		c.JSON(200, "index")
	})
	r.GET("/inter", func(c *gin.Context) {
		c.JSON(200, "inter")
	})
	r.GET("/user/get", func(c *gin.Context) {
		c.JSON(200, "/user/get")
	})

	r.GET("/user/del", func(c *gin.Context) {
		c.JSON(200, "/user/del")
	})

	r.GET("/user/:name", func(c *gin.Context) {
		c.JSON(200, "/user/:name")
	})

	r.Run(":9000")
}
func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++

	// 第一个api注册因为根节点path和children是空的所以直接成为根节点的子节点
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		//	获得第一次字符不同的位置 比如
		//  "/index" 和 "/inter" 第一次字符不同的位置 也就是 i=3
		i := longestCommonPrefix(path, n.path)

		if i < len(n.path) {
			child := node{
				path:      n.path[i:],
				wildChild: n.wildChild,
				indices:   n.indices,
				children:  n.children,
				handlers:  n.handlers,
				priority:  n.priority - 1,
				fullPath:  n.fullPath,
			}
			/*当前node增加一个子节点child 以 inter为例子,api inter插入之前已经有了index,并且发现他们有相同字符in所以将index节点改为 in节点,dex变成子in的子节点,后面的代码会再将 ter放到in子节点中。
			child := node{
				path:      n.path[i:],   // dex
				wildChild: n.wildChild,  // false
				indices:   n.indices,    // ""
				children:  n.children,   //null
				handlers:  n.handlers,   // index func
				priority:  n.priority - 1,
				fullPath:  n.fullPath,   // /index
			}
			n.indices = "d"
			n.path = "/in"
			n.fullPath = ""
			*/
			n.children = []*node{&child}
			n.indices = bytesconv.BytesToString([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		if i < len(path) {
			path = path[i:]
			c := path[0]

			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 以user/del为例;
			// 此时根节点的indices= "iu" 匹配到了相同字符 `u` 于是进行跳转,并将n指向 path="user/" 的节点
			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
				}
			}

			if c != ':' && c != '*' && n.nType != catchAll {
				//以插入inter api为例:
				// 此时n.indices  = "dt"  dex和ter的首字母
				n.indices += bytesconv.BytesToString([]byte{c})
				
				child := &node{
					fullPath: fullPath,
				}
				
				n.addChild(child)
				n.incrementChildPrio(len(n.indices) - 1)
				//这里这么写是方便后面流程通用  看 FF:
				n = child
			} else if n.wildChild {
				n = n.children[len(n.children)-1]
				n.priority++

				if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
					n.nType != catchAll &&
					(len(n.path) >= len(path) || path[len(n.path)] == '/') {
					continue walk
				}

				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
		}

		//FF:
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		n.fullPath = fullPath
		return
	}
}

最后这个前缀树的结构应该是这样的:

../../../img/2021/trie-gin.png

api请求

engine.trees这是一个数组,每个请求类型(POST GET PUT…)独立一个树

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree

当接收到底层传来的http请求后,先找到指定请求类型的树结构,然后再查询路由,查询方式比较简单,主要就是遍历树。为了提高查询效率,indices的作用就是在查询子节点之前,先找indices里有没有请求的path首字符,没有的话直接查询失败。

//gin.go

func (engine *Engine) handleHTTPRequest(c *Context) {
	//...
	httpMethod := c.Request.Method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		//查询路由
		value := root.getValue(rPath, c.params, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		//...
	}
}