Gin 的启动过程、路由及上下文源码解读

Engine

Engine 是 gin 框架的一个实例,它包含了多路复用器、中间件和配置中心。

Engine 的启动

gin 通过 Engine.Run(addr ...string) 来启动服务,最终调用的是 http.ListenAndServe(address, engine),其中第二个参数应当是一个 Handler 接口的实现,即 engine 实现了此接口:

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

Engine.ServeHTTP() 会先初始化一个空的上下文,然后挂上请求 c.Reuqest = req,随后执行 engine.handlerHTTPRequest(c)(包含主要处理逻辑的函数)。

  • engine.handlerHTTPRequest() 会先设置处理一些配置项,如 UseRawPath、RemoveExtraSlash 等
  • 然后开始寻找路由,从 engine.trees 中寻找
  • 当找到后执行找到路由对应的处理链 .handlers

以上就是正常处理一个请求的主要逻辑,其他的就现阶段来说先忽略了。

RouterGroup

Engine 组合了 RouterGroup。

RouterGroup 实现了 IRouter 接口,IRouter 接口是 IRoutes 接口和 Group 函数组合而成。

  • IRoutes 接口定义了所有路由处理的实现方法。
  • IRouter 接口定义了所有路由处理的实现方法以及一个分组方法(Group())。
// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}

// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
  // ...

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

RouterGroup 的结构

RouterGroup 的结构体只有四个属性:

type RouterGroup struct {
    Handlers HandlersChain // 创建时候会从父亲那 copy 一份,然后 append 指定的 handlers
    basePath string // 创建时候会从父亲那得到前缀,然后拼接指定的相对地址
    engine   *Engine // 会永远引用引擎
    root     bool // 标记位
}
  • .Handlers 属性是一个切片,按序存储着处理函数(中间件方法和最终的处理函数)
  • .basePath 属性是定位此 Group 的地址路径
  • .engine 属性总是指向 Engine 实例,且父子 Group 都存有相同的引用
  • .root 属性标记了此 Group 是否为根组,即最祖先的结点

当新建 Engine 时,会初始化一个 RouterGroup 结构,RouterGroup 是组合在 Engine 中的(所以 Engine 可以调用 RouterGroup 的所有方法),同时 Engine 的引用也记录在了 RouterGroup 上。

函数实现

如上,RouterGroup 实现了 IRouter 接口,下面是一些方法的实现。

  • Group() 方法,RouterGroup 通过 Group() 方法创建子分组,子分组会继承下来父 Group 的 handlers 并追加自己独有的 handlers,计算出此 Group 的 path 地址,及记录 Engine 地址。
  • POST(relativePath string, handlers ...HandlerFunc) 调用了 handle() 方法,是在 Group 中加一个路由(相对地址)及处理函数链(很常用就不多说了,其他类似方法也略)。
  • Handle(method, relativePath string, handlers ...HandlerFunc) 方法相对于 POST()/GET() 等方法只是可以传入自定义的方法名,用于特殊的、不标准的、Gin 内置不存在的的请求方法(不常用)。
  • Any() 会将路由及函数处理链在所有的支持方法上都 copy 存储一份,以实现通过任何请求方法都会有同样的调用链。
  • StaticFile(relativePath, filePath) 会将路由映射到文件系统的某一文件上,此时的 relativePath 是不允许有变量存在的(不允许有 : 和 * )。内部通过 c.File() 响应此文件。
  • Static(relativePath, root string) 将路由映射到文件系统的某一个文件夹上,底层调用了 StaticFS(relativePath, Dir(root, false))
  • StaticFS() 类似 Static(),但自定义 http.FileSystem 了,FileSystem 就可以理解为一个目录,这个目录就是所谓的文件系统。gin 的实现为了安全禁用了目录中的 list 功能。

StaticX 方法都加了路径中不允许存在变量(:*)的判断,所以使用是安全的。
var _ IRouter = &RouterGroup{} 可以用来检查 RouterGroup 是否实现了 IRouter 接口。👍

扩展:从 gin 对于 FileSystem 的实现可以探索更底层的东西。
gin.Dir(root string, listDirectory bool) 实现了对 http.Dir(root string) 的封装。
http.Dir() 用了本地文件系统的目录树,直接对外暴露一个文件夹有时候是不安全。比如文件中有些关键的隐藏文件等情况。
gin.Dir() 的第二个参数控制是否可以显示文件系统下的文件列表,默认 false 不显示,相对比较安全。
通过看源码发现 gin 是通过 onlyFilesFS.Readdir() 函数重写了 Readdir() 函数实现关闭 list 文件的。

Route 的添加

gin 通过上方 RouterGroup 暴露的几个方法添加路由,底层使用的方法是 Engine.addRoute(method, path string, handlers HandlerChain)

Engine.trees 属性是存储所有路由信息的总入口。它是一个切片,其中每个元素对应一种 method 并且是一个多叉树的根节点。

  • Engine.trees 是一个数组 []methodTree
  • methodTree{method string, root *node}
  • node{} 是个结点

当 addRoute 时,先根据 method 找到对应的 tree (Engine.trees[i])。然后会比较 加入者 的 path 和 node 中的 path 相似的部分,相似的部分 作为 父结点,不同的部分作为 子结点。以 多叉树 的方式存储下来。

这里会把 URL 中的路由变量也当作字符串存入树中,因为相同 URL 他们的变量也是一样的。

Route 的匹配

当请求进来时,因为 Engine 实现了 Handler 接口,所以最后会调用到 Engine.ServeHTTP 内。

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

gin 的 ServeHTTP 源码中可以看到获取它的 gin.Context 是通过池实现的,获取之后重置 ctx 中的信息。

找路径在

func (engine *Engine) handleHTTPRequest(c *Context) {
    // ...
    value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
    // ...
}

root.getValue() 比较复杂,这里就不多解释了。

gin 用的是 julienschmidt/httprouter 库的支持,所以可以参考这里。

Context

gin@v1.7.7 context.go

Context 中定义了一些属性和方法,用于扩展一些功能。

创建类方法

可以看到,这些方法主要用来获取 gin 自身 Context 的一些信息。

  • Copy() ,复制一个 Context 用于 goroutine 使用
  • HanderName() 方法,通过反射实现的获取 handler 的名字(以 包路径.NAME 的形式)
  • HandlerNames() 方法,返回完整的 handlers 链(这个真是相见恨晚的方法,可以用来调试一些调试中间件的一些异常问题,尤其是中间件中的数据传递问题)
  • Handler() 方法,返回主处理方法(最后一个)
  • FullPath() 方法,返回路由的 URL 全路径

HanderName() 的主要实现是通过反射方法获取到函数的名称:
runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()

reflect.ValueOf(f).Pointer() 返回 f 的 uintptr 值
runtime.FuncForPC() 将 PC(program counter,程序计数器地址)解释为 *Func 类型,即 Go 中函数在运行时的二进制表示
*Func 上有三个函数

  • Name() 返回函数全名(包地址+函数名)
  • Entry() 返回函数的 uintptr 地址
  • FileLine(pc uintptr) 返回 pc 指针的文件名所在行号

流程控制类方法

Context 中保存了所有 handlers 列表,存在 Context.handlers 数组中,并用下标 Context.index 标记当前执行的位置。
当主动取消调用链时,会将 index 设置成一个最大值 63(math.MaxInt8 / 2),也即调用链最大支持 64 个函数。
Context 中还提供了其他一些函数,当取消调用链的时候,可以设置请求返回的状态码和返回数据信息等。

  • Next() 方法,用于中间件中才有意义,用于在当前函数中开始执行下一个 handler
  • IsAborted() 方法,判断当前上下文是否已经取消
  • Abort() 方法,设置调用链为取消状态。
  • AbortWithError() 方法,= AbortWithStatus() + Error()
  • AbortWithStatus() 取消调用脸并设置 http 状态码
  • AbortWithStatusJSON() 方法,= Abort() + JSON()

Context 中的 httpWriter 整理一下。

错误处理

gin 在 Context 中定义了错误信息字段 Context.Errors 切片,可以链式存储错误信息。

  • Error(err error) *Error 用于将 err 追加到错误信息列表中。

元数据管理

Go 原生的 Context 是通过 ValueContext 来存储元数据信息的,每个 ValueContext 只能存储一对信息,存储多个信息对需要将许多 ValueContext 组成链条,读写很不高效。
gin 的 Context 中存的元数据数据是存在 Context.Keys map[string]interface{} 属性中的,比起原生的 Context 使用起来会更高效。

  • Set(key string, value interface{}) 设置键值对,存储到 Keys 属性中。
  • gin.Context 提供了比较丰富的获取各种类型数据的方法,如 GetString、GetTime、GetStringSlice、GetStringMapStringSlice 等等。

元数据的读和写是并发安全的。
重复设置某一个 key,会更新存储的 value。

输入数据

Param 类

是指用在 URL 路径中设置的参数,如 /user/:id 的 id 参数。
存储在 Context.Params 属性中,其本质是一个切片,每一个元素是一个 K/V 元组。
因此,在 URL 中是可以使用重复的变量名的(如 /test/:id/case/:id),但获取值就需要自己从属性中获取了(如:c.Params[0])。

  • Param(key) 用于获取单个 URL 内的参数。

解析请求中 URL Params 参数的位置是在 Engine.handleHTTPRequest()

Query 类

Query 类是用在 URL 后的参数部分(如:?id=1)。

gin 通过 Context.queryCache 属性存储 query 参数,在调用获取 Query 参数时以懒加载的方式初始化:c.queryCache = c.Request.URL.Query()

需要注意的是它也支持传入 map 和 array,map 的传入需要像这样 ?m[k1]=v1&m[k2]=v2,array 的传入像这样 ?a=1&a=2

  • Query(key),按 key 获取字符串参数值
  • DefaultQuery(key, defaultVal),获取字符串,当没有传时返回默认值。值得注意的是,这里“当没有传时”不包含传了但值为空的情况。即当传 ?a=&b=时,会取到空值而不是默认值。
  • QueryTYPE(),获取指定类型的数据,TYPE是个占位符,若无则返回空值。Type 可以是 Array、Map
  • GetQueryTYPE(key),比起 QueryType,这类函数会返回第二个参数表明参数中有没有设置

Form 类

包含 PostForm、FormFile、MultipartForm 等。
先略

  • FormFile() 获取用户提交的表单中的文件。
  • SaveUploadedFile() 将用户表单提交的文件保存到服务器文件系统中。
  • MultipartForm()

绑定引擎

gin 为方便使用,通过绑定引擎设置了自动绑定用户输入和结构数据的方法。

  • Bind() 按 Content-Type 自动绑定结构数据。支持类型 JSON / XML / YAML / Form-Data / ProtoBuf / MsgPack(见binding 包的 Default() 函数)。
  • BindJSON/BindXML/BindYAML/... 指定各自类型的绑定。
  • BindHeader() 绑定 header
  • BindUri 绑定 URL path 内的参数到结构体中

响应渲染

这里包含设置状态码、设置响应头以及等信息。

只说一些值得注意的

  • Header(k, v string) 设置响应的 header,当值为空字符串时,相当于删除此 header
  • IndentedJSON() 一般不要在正式环境中用,因为输出格式化的 JSON 是很耗 CPU 的事。
  • JSONP() xxx
  • DataFromReader() 由 gin 从 io 中读数据并写到响应
  • File() 高效率地写一个文件到响应。调用的是 http.ServeFile(),会禁用包含 .. 的路径
  • FileFromFS() 指定 FileSystem 响应文件内容
  • FileAttachment() 用指定的文件名响应客户端下载附件。
  • SSEvent() 写一个 Server-Send 事件到信息流中。
  • Stream() 流式响应

内容协商

  • Negotiate() 根据 Accept 的格式调用不同的 Render 方法
  • NegotiateFormat() 返回可接受到格式
  • SetAccepted() 设置 Accept header

实现 context.Context 接口

这些方法除了 .Value() 方法外,其他都是返回的默认空值,略。

其他

  • ClientIP() 返回用户 IP 地址,先获取 RemoteIP 然后从 Header 中获取真实 IP
  • RemoteIP() 返回 RemoteAddr
  • IsWebsocket() 通过 Header 判断是否为 websocket 请求
  • GetRawData() 获取流数据(c.Request.Body)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容