打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Gin框架源码解读

写在前面

我们今天就从下面这几行简单的代码中,探讨gin框架的底层实现 gin的底层是基于net/http包实现的,所以很多gin底层源码中涉及到了很多net/http的相关方法。

本文全部基于gin@v1.8.0进行讲解

package main

import "github.com/gin-gonic/gin"

func main() {
 r := gin.Default()
 r.GET("/ping"func(c *gin.Context) {
  c.JSON(200,"pong")
 })
 _ = r.Run(":3000")
}

1. Run 函数底层实现

  • gin/gin.go 文件
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.")
 }
(1) address := resolveAddress(addr)
 debugPrint("Listening and serving HTTP on %s\n", address)
(2) err = http.ListenAndServe(address, engine.Handler())
 return
}

这段代码还是比较容易看懂的。

  • address := resolveAddress(addr) 将传入的addr进行判断,返回正确的端口。
  • 调用http.ListenAndServe 对这个端口进行监听,并将框架的信息引擎传进入。

然后让我们来看看这个ListenAndServe的具体实现

  • net/http/server.go 文件

这个链接是基于TCP网络进行监听连接的,并且request和response都通过这个handler进行传递。

func ListenAndServe(addr string, handler Handler) error {
 server := &Server{Addr: addr, Handler: handler}
 return server.ListenAndServe()
}

然后我们来看一下这个Handler对象是如何实现处理请求和响应

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

这个Handler实现一个ServerHTTP的接口,来处理Response和Request,既然gin的Engine能和net/http包的Handler进行一个无缝连接,那么我们可以看看在这个gin包中,这个Engine是如何实现Handler()方法的。

接着我们来看一下这个gin的引擎对象 *Engine 实现的ServerHTTP方法

  • gin/gin.go 563行
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 c := engine.pool.Get().(*Context)
 // 创建上下文对象,注意这里是gin封装的Context,并不是go原生的Context!
 // 这里用到了sync.Pool来进行内存的复用,防止频繁创建上下文,而导致性能的下降
 c.writermem.reset(w)
 c.Request = req 
 // 对请求进行赋值,并将这个req请求放到context的Request上下文中。
 c.reset()

 engine.handleHTTPRequest(c) // 处理请求

 engine.pool.Put(c)  // 对上下文对象进行回收
}

那这个Engine的handleHTTPRequest() 方法究竟是怎么处理请求的呢?

  • gin/gin.go 585行
func (engine *Engine) handleHTTPRequest(c *Context) {
 httpMethod := c.Request.Method // 获取请求的方法
 rPath := c.Request.URL.Path // 获取URL请求地址
 {...对请求地址进行判断处理}

 t := engine.trees // 获取压缩前缀树数组,每个请求方法都有一棵radix树。
 for i, tl := 0len(t); i < tl; i++ {
  if t[i].method != httpMethod { 
  // 找到当前请求方式对应的radix树
   continue
  }
  root := t[i].root // 得到树的根节点
  value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
  // 根据请求路径获取匹配的redix树节点
  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
  }
  // {...请求后续处理,比如没有该方法之类的处理}
 c.handlers = engine.allNoRoute
 serveError(c, http.StatusNotFound, default404Body)
}

那你可能会对这个c.Next()感到疑惑,这个是如何对请求进行处理的呢?

  • gin/context.go 170 行
func (c *Context) Next() {
 c.index++ 
 // 指向要执行的中间件,初始值为-1,对这个index进行自增操作
 for c.index < int8(len(c.handlers)) {
 // 遍历所有的处理器,一次调用他们来处理请求
  c.handlers[c.index](c)
  // 使用中间件处理请求,中间件可以改变c.index的值
  c.index++
  // 然后再进行自增
 }
}

c.handlers 中是可以处理所有的路径请求,因为已经遍历完所有的c.index了,所以调用这个Next()就可以处理所有的命令。

然后我们看看这个Next()方法下面的另外两个方法IsAborted()Abort()

  • IsAborted() 判断是否已经终止处理器调用
func (c *Context) IsAborted() bool {
 return c.index >= abortIndex
}
  • Abort() 终止处理器调用
func (c *Context) Abort() {
 c.index = abortIndex
}

2. Engine 引擎对象初始化

我们一般会有两种形式的对象初始化,一个是 gin.New() 另一个是 gin.Default()

  • gin/gin.go 209行Default()其实就是New()之后新加了两个中间件而已
func Default() *Engine {
 debugPrintWARNINGDefault()
 engine := New()
 engine.Use(Logger(), Recovery())
 return engine
}

我们来重点看一下New()的实现。

func New() *Engine {
 debugPrintWARNINGNew()
 engine := &Engine{ // 初始化Engine对象
  RouterGroup: RouterGroup{ // 初始化路由组对象
   Handlers: nil,
   basePath: "/",
   root:     true// 设置该路由器组为根节点
  },
  
  FuncMap:                template.FuncMap{},
  RedirectTrailingSlash:  true,
  // 为true,如果只有/hello的路由存在,会将请求/hello/ 请求重定向到/hello , GET 响应到301, 其他响应到307。
  RedirectFixedPath:      false,
  // 如果找不到路由,尝试修复请求路径。例如 /HELLO 和 /../../HEllo 可以重定向到/hello。
  HandleMethodNotAllowed: false,
  // 是否对不允许的方法,做对应的响应;开启后,入股用POST方法请求[GET /user] ,请求将由[GET /user]处理
  
  ForwardedByClientIP:    true,
  RemoteIPHeaders:        []string{"X-Forwarded-For""X-Real-IP"},
  TrustedPlatform:        defaultPlatform,
  UseRawPath:             false,
  RemoveExtraSlash:       false,
  UnescapePathValues:     true,
  MaxMultipartMemory:     defaultMultipartMemory,
  // 提供给http.Request的ParseMultipartForm方法调用的“maxMerory”参数的值。默认是32MB
  trees:                  make(methodTrees, 09),
  // 创建容量为9的redix树切片,对应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 {
 // 设置 sync.Pool 新建上下文对象函数
  return engine.allocateContext()
 }
 return engine
}

3. Router路由

3.1 Group路由器组

r.Group() 可以帮助我们更快归纳某种请求。

  • gin/routergroup.go 文件

创建路由组,仅是返回路由组对象,路由组的本质就是一个模板,使用路由组添加路由,省去用户填写相同路径前缀和中间件的步骤

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
 // 返回一个路由组对象
 return &RouterGroup{
 // 新路由器组继承父路由器组的所有处理器
  Handlers: group.combineHandlers(handlers),
  basePath: group.calculateAbsolutePath(relativePath),
  // 将绝对路径计算成相对路径
  engine:   group.engine,
 }
}
  • combineHandlers()方法
type HandlersChain []HandlerFunc

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 finalSize := len(group.Handlers) + len(handlers)
 // 将原来的处理器长度加上放入当前需要追加的处理器长度
 assert1(finalSize < int(abortIndex), "too many handlers")
 // 如果超过了63中间件,这个路由是无法进行一个添加的,太多中间件要处理了。
 mergedHandlers := make(HandlersChain, finalSize)
 copy(mergedHandlers, group.Handlers)
 // 把旧的中间件都拷贝到新创建的切片中
 copy(mergedHandlers[len(group.Handlers):], handlers)
 // 把新的也追加到这个新的创建的切片中
 return mergedHandlers
}

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
 return joinPaths(group.basePath, relativePath)
 // 根据绝对路径进行拼接成相对路径
}

3.2 GET 路由

路由是怎么进行注册的呢?我们通过GET方法来了解一下是怎么处理的

  • gin/routergroup.go
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
 return group.handle(http.MethodGet, relativePath, handlers)
}

然后我们来看看这个handle()方法

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
 absolutePath := group.calculateAbsolutePath(relativePath)
 // 根据相对路径,计算绝对路径
 handlers = group.combineHandlers(handlers)
 // 合并处理器(实际上就是将handlers追加到原有的处理器组切片中,作为该路径的处理链)
 group.engine.addRoute(httpMethod, absolutePath, handlers)
 // 添加路由,涉及radix树添加节点方法。
 return group.returnObj()
}

4. Context 上下文

注意一点:这个上下文是Gin构建的,与Go原生的Context是不一样的

type Context struct {
 writermem responseWriter
 Request   *http.Request  // 请求对象
 Writer    ResponseWriter // 响应对象
 Params   Params    // 路由参数 /user/:id 这个id
 handlers HandlersChain  // 中间件数组 
 index    int8   // 当前执行中间件的下标
 fullPath string   // 请求的完整路径

 engine       *Engine
 params       *Params
 skippedNodes *[]skippedNode 

 mu sync.RWMutex  // 保证Keys map的线程安全
 Keys map[string]any // 对每一个请求进行处理存储

 Errors errorMsgs  // 存储错误的列表
 Accepted []string
 queryCache url.Values // 存放url请求参数
 formCache url.Values  // 存放form参数
 sameSite http.SameSite
}
  • Context创建
func (c *Context) reset() {
 c.Writer = &c.writermem
 c.Params = c.Params[:0]
 c.handlers = nil
 c.index = -1

 c.fullPath = ""
 c.Keys = nil
 c.Errors = c.Errors[:0]
 c.Accepted = nil
 c.queryCache = nil
 c.formCache = nil
 c.sameSite = 0
 *c.params = (*c.params)[:0]
 *c.skippedNodes = (*c.skippedNodes)[:0]
}
  • Context 传递过程进行拷贝

如果上下文在携程之间进行传递,那么必须要使用拷贝,传递副本。

因为context在处理完一个请求之后,就变成nil了,所以为了其他使用这个context的不报错,所以采用的是拷贝,防止被回收。拷贝是不会被回收的

func (c *Context) Copy() *Context {
 cp := Context{
  writermem: c.writermem,
  Request:   c.Request,
  Params:    c.Params,
  engine:    c.engine,
 }
 cp.writermem.ResponseWriter = nil
 cp.Writer = &cp.writermem
 cp.index = abortIndex
 cp.handlers = nil
 cp.Keys = map[string]any{}
 for k, v := range c.Keys {
  cp.Keys[k] = v
 }
 paramCopy := make([]Param, len(cp.Params))
 copy(paramCopy, cp.Params)
 cp.Params = paramCopy
 return &cp
}

5. 思考

  • 如果上下文对象的创建,可以用sync.Pool 来复用内存。
  • 如果上下文需要被并发使用,需要使用上下文副本。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
用Go+Vue.js快速搭建一个Web应用(初级demo)
Go web 框架 gin 的使用
Golang工程经验
通过Consul Raft库打造自己的分布式系统
Go context.WithCancel()的使用
在Golang中理解错误处理
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服