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)