1. 从简单例子分析底层原理

官方提供了一个quick start 例子,我们在此基础上增加使用路由分组和中间件,后续一行一行分析gin核心源码

  1. package main
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "net/http"
  5. )
  6. func main() {
  7. g := gin.Default()
  8. r := g.Group("/my")
  9. r.Use(helloMiddelware(), byeMiddleware())
  10. r.GET("/ping", func(c *gin.Context) {
  11. c.JSON(http.StatusOK, gin.H{
  12. "message": "pong",
  13. })
  14. })
  15. g.Run()
  16. }
  17. func helloMiddleware() gin.HandlerFunc {
  18. return func(c *gin.Context) {
  19. fmt.Println("hello...")
  20. c.Next()
  21. }
  22. }
  23. func byeMiddleware() gin.HandlerFunc {
  24. return func(c *gin.Context) {
  25. fmt.Println("bye...")
  26. c.Abort()
  27. }
  28. }

2. Engine实例的创建

我们先看第一行 g := gin.Default(),进入源码,可以看到是通过New()方法来创建Engine实例的。

  1. func Default() *Engine {
  2. debugPrintWARNINGDefault()
  3. engine := New()
  4. engine.Use(Logger(), Recovery())
  5. return engine
  6. }

我们重点看New()方法

  1. unc New() *Engine {
  2. // debug模式下的打印,可以忽略不看
  3. debugPrintWARNINGNew()
  4. engine := &Engine{
  5. // 顶端分组路由
  6. RouterGroup: RouterGroup{
  7. Handlers: nil,
  8. basePath: "/",
  9. root: true,
  10. },
  11. // 省略部分字段
  12. ...
  13. // 最大多媒体内存
  14. MaxMultipartMemory: defaultMultipartMemory,
  15. // 初始化一个容量为9的方法前缀树切片,对应9种请求方法
  16. trees: make(methodTrees, 0, 9),
  17. // 模板渲染分隔符
  18. delims: render.Delims{Left: "{{", Right: "}}"},
  19. // 省略部分字段
  20. ...
  21. }
  22. engine.RouterGroup.engine = engine
  23. // 为Engine的sync.Pool对象池指定生成对象的函数
  24. engine.pool.New = func() interface{} {
  25. return engine.allocateContext()
  26. }
  27. return engine
  28. }

从源码中我们可以看到New()方法主要做以下事情:

  • 为Engine实例初始化RouterGroup
  • 为Engine实例初始化MaxMultipartMemory
  • 为Engine实例初始化一个容量为9的方法前缀树切片trees
  • 为Engine实例初始化delims
  • 为Engine的sync.Pool对象池指定生成对象的函数

其中engine.allocateContext()主要是初始化Context实例:

  1. func (engine *Engine) allocateContext() *Context {
  2. v := make(Params, 0, engine.maxParams)
  3. skippedNodes := make([]skippedNode, 0, engine.maxSections)
  4. return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
  5. }

3. 路由分组的创建

Engine实例创建完成后,我们来看第二行代码r := g.Group("/my")

  1. // Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
  2. // For example, all the routes that use a common middleware for authorization could be grouped.
  3. func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
  4. return &RouterGroup{
  5. // 合并HandlerFunc
  6. Handlers: group.combineHandlers(handlers),
  7. // 根据相对路径拼接出路由分组的完整路径
  8. basePath: group.calculateAbsolutePath(relativePath),
  9. // 把原路由的Engine传递给新路由
  10. engine: group.engine,
  11. }
  12. }

Group()方法主要做了以下事情:

  • 原路由的Handlers和新加入的Handlers合并
  • 根据相对路径拼接出新路由分组的完整路径
  • 把原路由的Engine传递给新路由

我们可以稍微看看combineHandlers方法和calculateAbsolutePath:

  1. func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
  2. // 新Handlers的总长度
  3. finalSize := len(group.Handlers) + len(handlers)
  4. // 长度大于等于63,则panic
  5. if finalSize >= int(abortIndex) {
  6. panic("too many handlers")
  7. }
  8. // 创建长度为finalSize的HandlerFunc切片(HandlersChain其实是[]HandlerFunc的别名)
  9. mergedHandlers := make(HandlersChain, finalSize)
  10. // 把原路由的HandlerFunc和新加入的HandlerFunc拷贝到新切片
  11. copy(mergedHandlers, group.Handlers)
  12. copy(mergedHandlers[len(group.Handlers):], handlers)
  13. return mergedHandlers
  14. }
  1. func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
  2. // 根据原路由的路径和新的路径进行拼接
  3. return joinPaths(group.basePath, relativePath)
  4. }
  1. func joinPaths(absolutePath, relativePath string) string {
  2. if relativePath == "" {
  3. return absolutePath
  4. }
  5. finalPath := path.Join(absolutePath, relativePath)
  6. if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
  7. return finalPath + "/"
  8. }
  9. return finalPath
  10. }

4. 中间件

路由分组创建完成,我们可以使用Use方法来注册中间件:

  1. // Use adds middleware to the group, see example code in GitHub.
  2. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
  3. // 把中间件追加到Handlers切片中
  4. group.Handlers = append(group.Handlers, middleware...)
  5. return group.returnObj()
  6. }

实际上Use方法只做了一件事情,就是把中间件追加到Handlers切片。但值得注意的是,在helloMiddleware中间件中我们使用了c.Next()来使得处理链继续流转:

  1. // Next should be used only inside middleware.
  2. // It executes the pending handlers in the chain inside the calling handler.
  3. // See example in GitHub.
  4. func (c *Context) Next() {
  5. // 索引自增
  6. c.index++
  7. // 索引小于该树节点的Handlers就继续遍历
  8. for c.index < int8(len(c.handlers)) {
  9. // 执行HandlerFunc
  10. c.handlers[c.index](c)
  11. // 流转到下一个HandlerFunc
  12. c.index++
  13. }
  14. }

其实从源码可以分析出,就算中间件不使用Next方法,处理链也是会往下流转的。但是问题来了,多次调用Next方法不就会执行多个遍历吗?
由于是HandlerFunc是公用同一个Context的,A遍历中执行HandlerFunc导致B遍历执行,假设现在只有两个中间件,在B遍历结束时c.index是大于 handlers长度的,现在回到了A,A执行代码c.index++必定大于handlers长度,所以A遍历不会往下执行,c.handlers也就只会被遍历一次。
c.Abort()用于终止处理链:

  1. func (c *Context) Abort() {
  2. c.index = abortIndex
  3. }

在路由分组创建中我们知道handlers最大长度是62,所以通过设置c.index=63即可终止handlers的遍历

5. 注册路由

从源码看,其实GET, POST, PUT, DELETE等方法都是通过group.handle处理:

  1. func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  2. // 拼接路径
  3. absolutePath := group.calculateAbsolutePath(relativePath)
  4. // 追加Handlers
  5. handlers = group.combineHandlers(handlers)
  6. // 把路由添加到路由树
  7. group.engine.addRoute(httpMethod, absolutePath, handlers)
  8. return group.returnObj()
  9. }
  1. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  2. // 路径,方法,Handlers的校验
  3. assert1(path[0] == '/', "path must begin with '/'")
  4. assert1(method != "", "HTTP method can not be empty")
  5. assert1(len(handlers) > 0, "there must be at least one handler")
  6. debugPrintRoute(method, path, handlers)
  7. // 获取该方法树的根节点
  8. root := engine.trees.get(method)
  9. // 如果没有,创建一个
  10. if root == nil {
  11. root = new(node)
  12. root.fullPath = "/"
  13. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  14. }
  15. // 方法树中添加节点
  16. root.addRoute(path, handlers)
  17. // 省略部分代码
  18. ...
  19. }

engine.addRoute主要做两件事:

  • 获取方法树的根节点
  • 在方法树中新增节点

6. 处理请求

gin运行服务和处理请求其实都是使用net/http包的相关方法,只是做了封装而已:

  1. // Run attaches the router to a http.Server and starts listening and serving HTTP requests.
  2. // It is a shortcut for http.ListenAndServe(addr, router)
  3. // Note: this method will block the calling goroutine indefinitely unless an error happens.
  4. func (engine *Engine) Run(addr ...string) (err error) {
  5. defer func() { debugPrintError(err) }()
  6. if engine.isUnsafeTrustedProxies() {
  7. debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
  8. "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
  9. }
  10. // 处理IP地址
  11. address := resolveAddress(addr)
  12. debugPrint("Listening and serving HTTP on %s\n", address)
  13. // 通过net/http的方法来运行socket服务
  14. err = http.ListenAndServe(address, engine)
  15. return
  16. }

我们知道,ListenAndServe方法的第二个参数是Handler接口,也就意味着Engine实现了该接口

  1. // ServeHTTP conforms to the http.Handler interface.
  2. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3. // 从对象池中获取一个Context对象
  4. c := engine.pool.Get().(*Context)
  5. // 初始化Context的相关字段
  6. c.writermem.reset(w)
  7. c.Request = req
  8. c.reset()
  9. // 处理请求
  10. engine.handleHTTPRequest(c)
  11. // 回收对象
  12. engine.pool.Put(c)
  13. }

这里真正处理请求的是handleHTTPRequest方法

  1. func (engine *Engine) handleHTTPRequest(c *Context) {
  2. // 省略部分代码
  3. ...
  4. // Find root of the tree for the given HTTP method
  5. t := engine.trees
  6. // 查找该请求对应的方法树
  7. for i, tl := 0, len(t); i < tl; i++ {
  8. if t[i].method != httpMethod {
  9. continue
  10. }
  11. root := t[i].root
  12. // 在方法树中查找路由
  13. // Find route in tree
  14. value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
  15. if value.params != nil {
  16. c.Params = *value.params
  17. }
  18. if value.handlers != nil {
  19. // 把路由的handlers和完整路径赋值给当前上下文
  20. c.handlers = value.handlers
  21. c.fullPath = value.fullPath
  22. // 遍历handlers处理请求
  23. c.Next()
  24. // 写响应头部
  25. c.writermem.WriteHeaderNow()
  26. return
  27. }
  28. if httpMethod != "CONNECT" && rPath != "/" {
  29. if value.tsr && engine.RedirectTrailingSlash {
  30. redirectTrailingSlash(c)
  31. return
  32. }
  33. if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
  34. return
  35. }
  36. }
  37. break
  38. }
  39. // 如果不允许该请求方法处理,则在其他方法树中查找路由进行处理
  40. if engine.HandleMethodNotAllowed {
  41. for _, tree := range engine.trees {
  42. if tree.method == httpMethod {
  43. continue
  44. }
  45. if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
  46. c.handlers = engine.allNoMethod
  47. serveError(c, http.StatusMethodNotAllowed, default405Body)
  48. return
  49. }
  50. }
  51. }
  52. // 没有在树中找到路由,则返回404
  53. c.handlers = engine.allNoRoute
  54. serveError(c, http.StatusNotFound, default404Body)
  55. }

7. 上下文

gin中的Context虽然实现了golang的context.Context接口,但是只有Value方法做了实现,它更多的是用来承载请求的字段和相应的处理方法,例如请求参数的获取和绑定

  1. // Deadline always returns that there is no deadline (ok==false),
  2. // maybe you want to use Request.Context().Deadline() instead.
  3. func (c *Context) Deadline() (deadline time.Time, ok bool) {
  4. return
  5. }
  6. // Done always returns nil (chan which will wait forever),
  7. // if you want to abort your work when the connection was closed
  8. // you should use Request.Context().Done() instead.
  9. func (c *Context) Done() <-chan struct{} {
  10. return nil
  11. }
  12. // Err always returns nil, maybe you want to use Request.Context().Err() instead.
  13. func (c *Context) Err() error {
  14. return nil
  15. }
  16. // Value returns the value associated with this context for key, or nil
  17. // if no value is associated with key. Successive calls to Value with
  18. // the same key returns the same result.
  19. func (c *Context) Value(key interface{}) interface{} {
  20. if key == 0 {
  21. return c.Request
  22. }
  23. if keyAsString, ok := key.(string); ok {
  24. val, _ := c.Get(keyAsString)
  25. return val
  26. }
  27. return nil
  28. }

需要注意的是Copy方法,gin不建议在协程中使用Context,如果必须要使用也是使用副本。为什么呢?我们之前也分析到Context对象是从pool中获取的,当请求结束会回收Context,如果此时在另外的协程中使用Context就会导致panic。