在上一篇文章 Gin 源码学习(三)丨路由是如何构建和匹配的? 中,讲解了 Gin 的路由是如何实现的,那么,当路由成功匹配后,或者匹配失败后,在 Gin 内部会对其如何处理呢?
在这一篇文章中,将讲解 Gin 对一个 HTTP 请求的具体处理流程是怎样的。
下面,将对一个请求进入 Gin 的处理范围后的内容,进行一步步展开,讲解 Gin 对请求的处理流程。
Go 版本:1.14
Gin 版本:v1.5.0
目录
- 请求的处理流程
- 小结
请求的处理流程
在上一篇文章中,我们讲到 Gin 其实实现了 Go 自带函数库 net/http
库中的 Handler
接口,并且从实现的源代码中可以发现,当一个 HTTP 请求到达 Gin 处理的范围时,首先是在 Gin 的 Engine
类型中的 ServeHTTP(w http.ResponseWriter, req *http.Request)
方法中对 Gin 保存上下文信息的 gin.Context
进行属性设置和重置操作,然后才是使用 engine.handleHTTPRequest(c *Context)
方法来对 HTTP 请求进行处理的,下面,我们一步一步来对相关源代码进行分析:
// ServeHTTP conforms to the http.Handler interface.
// 符合 http.Handler 接口的约定
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取已存在的上下文对象
c := engine.pool.Get().(*Context)
// 重置该上下文对象的 ResponseWriter 属性
c.writermem.reset(w)
// 设置该上下文对象的 Request 属性
c.Request = req
// 重置上下文中的其他属性信息
c.reset()
// 对请求进行处理
engine.handleHTTPRequest(c)
// 将该上下文对象重新放回对象池中
engine.pool.Put(c)
}
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
// 上下文是 Gin 最重要的部分.
// 它允许我们在中间件之间传递变量, 管理流程, 例如验证请求的 JSON 并呈现 JSON 响应.
type Context struct {
// 对 net/http 库中的 ResponseWriter 进行了封装
writermem responseWriter
// 请求对象
Request *http.Request
// 非 net/http 库中的 ResponseWriter
// 而是 Gin 用来构建 HTTP 响应的一个接口
Writer ResponseWriter
// 存放请求中的 URI 参数
Params Params
// 存放该请求的处理函数切片, 包括中间件加最终处理函数
handlers HandlersChain
// 用于标记当前执行的处理函数
index int8
// 请求的完整路径
fullPath string
// Gin 引擎对象
engine *Engine
// Keys is a key/value pair exclusively for the context of each request.
// 用于上下文之间的变量传递
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
// 与处理函数/中间件对应的错误列表
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
// 接受格式列表
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
// 用于缓存请求的 URL 参数
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
// 用于缓存请求体中的参数
formCache url.Values
}
复制代码
上面源代码中,展示了 engine.ServeHTTP(w http.ResponseWriter, req *http.Request)
方法的执行过程以及 gin.Context
类型的内部结构,需要注意的是,gin.Context
实现了 Go 的 Context
接口,但是并没有对其做并发安全处理,因此,应该避免多个 goroutine 同时访问同一个 Context,如果存在这种情况,需使用 gin.Context.Copy()
方法,对 gin.Context
进行复制使用。
并且,Gin 使用对象池来存放上下文信息,这是一个非常巧妙的设计思想,因为在 Gin 中,会将请求的许多处理信息存放于 gin.Context
中,而 Go 是一门带有 GC(垃圾回收)的语言,假如在访问量较大的场景下,如果不使用对象池来缓冲 gin.Context
对象的话,那么为每一个请求创建一个 gin.Context
对象,并且在完成请求的处理后,将该 gin.Context
对象交给 GC 去处理,这无疑对 GC 增添了许多压力。由于 gin.Context
只是用于保存当前请求的处理信息,用于上下文之间的参数传递,属于完全可以复用的对象,因此,使用对象池对其进行存放可以在一定程度上减少 GC 压力。
下面先来看一下在 engine.ServeHTTP(w http.ResponseWriter, req *http.Request)
方法中对 gin.Context
初始化时设置了什么样的初始值:
const (
// 表示未写入
noWritten = -1
// 200 状态码
defaultStatus = http.StatusOK
)
type responseWriter struct {
// net/http 库中的 ResponseWriter
http.ResponseWriter
// 响应内容大小
size int
// 响应状态码
status int
}
func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer
w.size = noWritten
w.status = defaultStatus
}
func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
}
复制代码
这里需要留意的是 gin.Context.index
的初始值,Gin 通过该值来调用处理函数和判断当前上下文是否终止。
下面,我们来看 Gin 对请求的处理流程,先来看一下 engine.handleHTTPRequest(c *Context)
方法,该方法在前面的几篇 Gin 源码学习的文章中出现过多次,所以这里也是同样,只保留其与当前文章主题相关的源代码:
func (engine *Engine) handleHTTPRequest(c *Context) {
// 省略...
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
// 开始对请求执行中间件和处理函数
c.Next()
// 设置响应头信息
c.writermem.WriteHeaderNow()
return
}
// 省略...
break
}
// 省略...
c.handlers = engine.allNoRoute
// 处理 404 错误
serveError(c, http.StatusNotFound, default404Body)
}
复制代码
在上一篇文章中讲到过 Gin 请求路由的匹配是在 root.getValue(path string, po Params, unescape bool)
方法中实现的,并且,当返回的 value
对象中的 handlers
属性不为 nil
时,则表示该请求存在处理函数,然后将 value
对象中的处理函数切片集、请求参数以及请求的完整路径信息存放至该请求的上下文对象中,接着调用 context.Next()
方法,下面来看一下该方法的源代码:
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// Next 只能在中间件内部使用
// 它在调用处理程序内的链中执行挂起的处理程序
// 类似于递归调用或函数装饰器
func (c *Context) Next() {
// index 初始值为 -1
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
复制代码
context.Next()
方法的逻辑比较简单,其实就是遍历存放于 Gin 上下文中的中间件/处理函数切片,并调用,在上一篇文章中,我们也讲过,context.handlers
切片中,在有多个 HandlerFunc
的时候,除了最后一个为该路由的处理函数之外,其余的都为中间件。
这里需要注意的点是,该 Next()
方法,在使用的时候,只能在中间件内部使用,也就是说,在日常开发中,该方法只能在自己编写的中间件中出现,而不能出现在其它地方。
下面,我们以 gin.Default()
创建 gin.Engine
时添加的两个默认中间件 Logger
和 Recovery
,并结合一个模拟身份验证的中间件 Auth
为例,来对 Gin 中间件的工作流程进行详细讲解,先来看一下 gin.Default()
方法添加的默认中间件 Logger
的相关源代码:
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
// Logger 是一个中间件, 该中间件会将日志写入 gin.DefaultWriter.
// 默认的 gin.DefaultWriter 为 os.Stdout, 即标准输出流, 控制台
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
// LoggerConfig defines the config for Logger middleware.
// Logger 中间件的相关配置
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter
// 用于输出内容的格式化, 默认为 gin.defaultLogFormatter
Formatter LogFormatter
// Output is a writer where logs are written.
// Optional. Default value is gin.DefaultWriter.
// 日志输出对象
Output io.Writer
// SkipPaths is a url path array which logs are not written.
// Optional.
// 忽略日志输出的 URL 切片
SkipPaths []string
}
// LoggerWithConfig instance a Logger middleware with config.
// 使用 LoggerConfig 配置的 Logger 中间件
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
formatter := conf.Formatter
if formatter == nil {
formatter = defaultLogFormatter
}
out := conf.Output
if out == nil {
out = DefaultWriter
}
notlogged := conf.SkipPaths
// 是否输出至终端
isTerm := true
if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
isTerm = false
}
// 标记忽略日志的 path
var skip map[string]struct{}
if length := len(notlogged); length > 0 {
skip = make(map[string]struct{}, length)
for _, path := range notlogged {
skip[path] = struct{}{}
}
}
return func(c *Context) {
// Start timer
// 记录开始时间
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
// Process request
// 继续执行下一个中间件
c.Next()
// Log only when path is not being skipped
// 如果 path 在 skip 中, 则忽略日志记录
if _, ok := skip[path]; !ok {
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer
// 记录结束时间
param.TimeStamp = time.Now()
// 计算耗时
param.Latency = param.TimeStamp.Sub(start)
// 客户端 IP
param.ClientIP = c.ClientIP()
// 请求方法
param.Method = c.Request.Method
// 请求状态码
param.StatusCode = c.Writer.Status()
// 错误信息
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
// 响应体大小
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
param.Path = path
// 日志打印
fmt.Fprint(out, formatter(param))
}
}
}
复制代码
其实中间件也就是装饰器或闭包,实质上就是一种返回类型为 HandlerFunc
的函数,通俗地讲,就是一种返回函数的函数,目的就是为了在外层函数中,对内层函数进行装饰或处理,然后再将被装饰或处理后的内层函数返回。
由于 HandlerFunc
函数只能接受一个 gin.Context
参数,因此,在上面源代码中的 LoggerWithConfig(conf LoggerConfig)
函数中,使用 LoggerConfig
配置,对 HandlerFunc
进行装饰,并返回。
同样地,在返回的 HandlerFunc
匿名函数中,首先是记录进入该中间件时的一些信息,包括时间,然后再调用 context.Next()
方法,挂起当前的处理程序,递归去调用后续的中间件,当后续所有中间件和处理函数执行完毕时,再回到此处,如果要记录该 path
的日志,则再获取一次当前的时间,与开始记录的时间进行计算,即可得出本次请求处理的耗时,再保存其它信息,包括请求 IP 和响应的相关信息等,最后将该请求的日志进行打印处理,这就是使用 gin.Default()
实例一个 gin.Engine
默认添加的 Logger()
中间件的处理流程。
下面,我们来看一下,另一个默认中间件 Recovery()
的相关源代码:
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// Recovery 中间件用于捕获处理流程中出现 panic 的错误
// 如果连接未断开, 则返回 500 错误响应
func Recovery() HandlerFunc {
// DefaultErrorWriter = os.Stderr
return RecoveryWithWriter(DefaultErrorWriter)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
// 使用传递的 out 对 Recovery 中间件进行装饰
func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
// 用于标记连接是否断开
var brokenPipe bool
// 从错误信息中判断连接是否断开
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// 省略, 日志打印相关...
// If the connection is dead, we can't write a status to it.
if brokenPipe { // 如果连接已断开, 则已经无法为其写入状态码
// 添加错误信息至上下文中, 用于日志输出
c.Error(err.(error)) // nolint: errcheck
// 终止该上下文
c.Abort()
} else { // 连接未断开
// 终止该上下文并写入 500 错误状态码
c.AbortWithStatus(http.StatusInternalServerError)
}
}
}()
// 继续执行下一个中间件
c.Next()
}
}
复制代码
与 LoggerWithConfig(conf LoggerConfig)
函数一样,RecoveryWithWriter(out io.Writer)
函数仅为了对最终返回的中间件 HandlerFunc
函数进行装饰,在该中间件中,可分为两个逻辑块,一个是 defer
,一个是 Next()
,Next()
与 Logger()
中间件中的 Next()
作用类似,这里在 defer
中使用 recover()
来捕获在后续中间件中 panic
的错误信息,并对该错误信息进行处理。
在该中间件中,首先是判断当前连接是否已中断,然后是进行相关的日志处理,最后,如果连接已中断,则直接设置错误信息,并终止该上下文,否则,终止该上下文并返回 500 错误响应。
下面,我们来看一下 context.Abort()
方法和 context.AbortWithStatus(code int)
方法的相关源代码:
// 63
const abortIndex int8 = math.MaxInt8 / 2
// 终止上下文
func (c *Context) Abort() {
c.index = abortIndex
}
// 判断上下文是否终止
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
// 终止上下文并将 code 写入响应头中
func (c *Context) AbortWithStatus(code int) {
c.Status(code)
c.Writer.WriteHeaderNow()
c.Abort()
}
复制代码
context.Abort()
方法将当前上下文的 index
值设置为 63,用于标志上下文的终止。
context.AbortWithStatus(code int)
也是终止当前的上下文,只不过额外的使用了 code
参数,对响应的头信息进行了设置。
最后,我们再来看一个模拟身份校验的中间件 Auth
,其实现的相关源代码如下:
type RequestData struct{
Action string `json:"action"`
UserID int `json:"user_id"`
}
func main() {
router := gin.Default()
// 注册中间件
router.Use(Auth())
router.POST("/action", func(c *gin.Context) {
var RequestData RequestData
if err := c.BindJSON(&RequestData); err == nil {
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success"})
}
})
router.Run(":8000")
}
func Auth() gin.HandlerFunc {
// TODO: 可模仿 Logger() 或 Recovery() 中间件, 结合该函数的调用参数, 在此处做一些配置操作
return func(c *gin.Context) {
// TODO: 可模仿 Logger() 中间件, 在此处对请求的 path 进行忽略处理
if auth(c.Request) {
c.Next()
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"})
}
}
}
func auth(req *http.Request) bool {
// TODO: 对 http.Request 中的信息进行校验, 如 cookie, token...
return req.Header.Get("Auth") == "colelie"
}
复制代码
首先是 Auth()
函数,该函数用于装饰并返回 gin.HandlerFunc
函数,在该函数内,返回了一个 gin.HandlerFunc
匿名函数,在该匿名函数中,通过调用 auth(req *http.Request)
函数对请求信息进行校验,这里只是一个简单地对请求头中的 Auth
进行验证。
所以,在该案例中,当我们访问 /action
接口时,首先会进入 Logger()
中间件,然后进入 Recovery()
中间件,再进入 Auth()
中间件,当前面的中间件都没有发生对上下文的终止操作时,才会进入我们声明的 router.POST("/action", func)
处理函数。
当我们向 /action
接口发起一个普通的 POST 请求时,会收到如下响应:
这是由于在 Auth()
中间件中身份校验没通过,我们为该请求的头部信息中添加一个 Key 为 Auth
,Value 为 colelie
的字段,会收到如下响应:
可以发现,同样的,出现了错误响应,而这次的错误响应码为 400,这是为什么呢?
在 Gin 源码学习(二)丨请求体中的参数是如何解析的? 中,我们讲过,在使用 MustBind
一类的绑定函数时,如果在参数解析过程中出现错误,会调用 c.AbortWithError(http.StatusBadRequest, err)
方法,终止当前的上下文并返回 400 响应错误码,在上面的声明的对 /action
的处理函数中,使用了 context.BindJSON(obj interface{})
方法对请求参数进行绑定操作,下面,我们在为请求添加能够绑定成功的请求体,会收到如下响应:
这次,得到了正确的响应内容。
小结
在这篇文章中,我们围绕 gin.Context
的内部结构、Gin 中间件和处理函数的工作流程,讲解了 Gin 对请求的处理流程。
首先,在 gin.Engine
中,使用对象池 sync.Pool
来存放 gin.Context
这样做的目的是为 Go GC 减少压力。
然后,在 Gin 内部,当路由匹配成功后,将调用 context.Next()
方法,开始进入 Gin 中间件和处理函数的执行操作,并且,需要注意的是,在日常开发中,该方法,只能在中间件中被调用。
最后,以使用 gin.Default()
方法创建 gin.Engine
时携带的两个默认中间件 Logger()
和 Recovery()
,和我们自己编写的一个模拟身份校验的中间件 Auth()
,结合注册的 path 为 /action
的路由,对 Gin 中间件和处理函数的工作流程进行了讲解。
至此,Gin 源码学习的第四篇也就到此结束了,感谢大家对本文的阅读~~
欢迎扫描以下二维码关注笔者的个人订阅号,获取最新文章推送:
有疑问加站长微信联系(非本文作者)