【Gin 源码阅读】Gin 的上下文

2,105 阅读6分钟

gin的上下文

为什么要设计Context呢?

  1. 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

看看封装后的前后差距:

net/http库:

obj = map[string]interface{}{
    "name": "geektutu",
    "password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
    http.Error(w, err.Error(), 500)
}

使用gin框架:

c.JSON(200, gin.H{
			"message": "pong",
		})
  1. 针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

Context

r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

只要你写handlerFunc,一般你的参数都是c *gin.Context,今天我们来揭开Context的神秘面纱!👀👀👀

Context结构如下:

// 上下文是 gin 中最重要的部分。 它允许我们在中间件之间传递变量,
// 管理流程,例如验证请求的 JSON 并呈现 JSON 响应。
type Context struct {
    //初始结构
	writermem responseWriter//存储http.ResponseWriter等信息
	Request   *http.Request//http.Request信息
	Writer    ResponseWriter//存储http.ResponseWriter的相关接口
	
    //用于获取param的参数
	Params   Params
    //存储HandlerFunc,执行中间件与执行函数
	handlers HandlersChain
    //记录当前执行中间件的第几个
	index    int8
    //请求信息的路径
	fullPath string

	engine *Engine
	params *Params

	// 这个互斥锁保护密钥映射
	mu sync.RWMutex

	// Keys 是专门用于每个请求上下文的键/值对。
	Keys map[string]interface{}

	// Errors 是附加到所有使用此上下文的处理程序/中间件的错误列表。
	Errors errorMsgs

	// Accepted 定义了手动接受的内容协商格式列表。
	Accepted []string

	// queryCache 使用 url.ParseQuery 缓存来自 c.Request.URL.Query() 的 param 查询结果
	queryCache url.Values

// formCache 使用 url.ParseQuery 缓存 PostForm 包含从 POST、PATCH、
// 或 PUT 主体参数。
	formCache url.Values

	// SameSite 允许服务器定义 cookie 属性,使其无法
// 浏览器将此 cookie 与跨站点请求一起发送。
	sameSite http.SameSite
}

Context对象定义的字段真的是太多了,也不清楚作用都是啥?

先别着急,我们往后看,看他的接口,就会明白他的这些字段的作用是哪些了!!!😁😁😁

当你进入context.go文件里面,发现除了定义了一个Context对象时候,其他的都是接口,特别多,已经达到了上千行。

你需要明白我们此行的目的是去看我们在利用gin框架写后端代码时候常用的一些接口函数

构造函数

/************************************/
/********** 上下文创建********/
/************************************/

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
	*c.params = (*c.params)[0:0]
}

这个好像是上下文的构造函数,可是我们在写HandlerFunc 根本没有创建过,都是直接当作参数去使用,那么他在哪儿去调用呢?

Context的主要作用是

  • 简化构造一个完整响应的过程
  • 路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例,

这些作用好像都建立在我发起一个请求,然后才会体现出来的。

在之前一篇的《gin是如何运行的》文章里面,知道了一个重要的因素

只要传入任何实现了ServeHTTP接口的实例,所有HTTP的请求,就都交给了该实例去处理。

所以Context这个对象的构建会不会在ServerHTTP这个接口里面啊?

我们再来看看这个ServerHTTP的接口

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    //从池子中建立一个空白的Context对象
	c := engine.pool.Get().(*Context)
    //初始化responseWriter的http.ResponseWriter和内容大小(-1)默认返回码
	c.writermem.reset(w)
    //初始化Context的Request
	c.Request = req
    //构造函数
	c.reset()

	engine.handleHTTPRequest(c)
	//释放刚才建立的c
	engine.pool.Put(c)
}

在这里我们明白了,Context的一些字段的作用

  • writermem
    • 存储http.ResponseWriter以及内容大小和返回码
  • Request
    • 存储http.Request信息
  • Writer
    • ResponseWriter相关接口

获取参数

常用的获取参数函数

  • c.Query 获取querystring参数
  • c.PostForm 获取form表单参数数据
  • c.GetRawData 获取json格式数据
  • c.Param 获取path参数

c.Query

// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /path?id=1234&name=Manu&value=
// 	   c.Query("id") == "1234"
// 	   c.Query("name") == "Manu"
// 	   c.Query("value") == ""
// 	   c.Query("wtf") == ""
func (c *Context) Query(key string) string {
	value, _ := c.GetQuery(key)
	return value
}

根据注释来看,我们写的Query 可以等价于c.Request.URL.Query().Get(key) 在可以等价于http.Request.URL.Query().Get(key)

为什么可以这样子等价呢?我们来看下面的代码

func (c *Context) initQueryCache() {
	if c.queryCache == nil {
		if c.Request != nil {
			c.queryCache = c.Request.URL.Query()
		} else {
			c.queryCache = url.Values{}
		}
	}
}

说到最后他是利用原生的方式获取Query然后存储在queryCache这个字段里面

// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
	c.initQueryCache()
	if values, ok := c.queryCache[key]; ok && len(values) > 0 {
		return values, true
	}
	return []string{}, false
}

然后通过key拿到queryCache 里面的元素

通过以上过程的猜测,queryCache是一个map类型的。

最后发现确实如此type Values map[string][]string

至于最后怎么拿到Query的参数,建议去看原生库的实现!!

c.PostForm

这个实现和上面的c.Query一样的过程。贴一些重要的代码

func (c *Context) initFormCache() {
	if c.formCache == nil {
		c.formCache = make(url.Values)
		req := c.Request
        //通过原生ParseMultipartForm去获取post提交的表单参数
        //c.engine.MaxMultipartMemory限制提取的最大体积
        //c.engine.MaxMultipartMemory=defaultMultipartMemory = 32 << 20 // 32 MB
		if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
			if err != http.ErrNotMultipart {
				debugPrint("error on parse multipart form array: %v", err)
			}
		}
		c.formCache = req.PostForm
	}
}

c.GetRawData

他可能就比较简单了就直接读取body内部的内容信息

// GetRawData return stream data.
func (c *Context) GetRawData() ([]byte, error) {
	return ioutil.ReadAll(c.Request.Body)
}

我们顺便可以看看ReadAll()如何实现的

func ReadAll(r Reader) ([]byte, error) {
	b := make([]byte, 0, 512)
	for {
		if len(b) == cap(b) {
			// Add more capacity (let append pick how much).
			b = append(b, 0)[:len(b)]
		}
		n, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+n]
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}
	}
}

这个函数主要表达的是我不断读取信息,直到我读到错误或者EOF我就会返回数据和err。当我成功读完时候是err=nil,当我读到EOF,我不会将读取的EOF作为要返回的错误

c.Param

// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string {
	return c.Params.ByName(key)
}

真正的核心代码

// Param 是单个 URL 参数,由键和值组成。 
type Param struct {
	Key   string
	Value string
}

// Params is a Param-slice, as returned by the router.
// The slice is ordered, the first URL parameter is also the first slice value.
// It is therefore safe to read values by the index.
type Params []Param

// Get 返回与给定名称匹配的第一个参数的值。
// 如果没有找到匹配的Param,则返回一个空字符串。
func (ps Params) Get(name string) (string, bool) {
	for _, entry := range ps {
		if entry.Key == name {
			return entry.Value, true
		}
	}
	return "", false
}

当你发起请求时候他会将URL的参数进行分解,存储在[]Param这个切片里面,然后从中取值。

至于他怎么去进行分解,等讲到路由再说!

其实,我写项目一般都是用的是

  • ShouldBind
  • ShouldBindJSON

会代替上面一部分的函数。

其实这些ShouldBind这类函数都是gin内置了库(github.com/go-playgrou…

核心代码

//shouldBind实现函数
func (c *Context) ShouldBind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.ShouldBindWith(obj, b)
}

// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
	return c.ShouldBindWith(obj, binding.JSON)
}
//上面两个函数都返回的 函数源码
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}
//解释上面的函数Bind
//相关接口与实现的函数
type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}
//JSON类型与实现的接口函数
var JSON          = jsonBinding{}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
	if req == nil || req.Body == nil {
		return fmt.Errorf("invalid request")
	}
    //检验json最后利用就是validator库进行检验
    //具体作用就是根据tag检查req.Body是否满足要求
	return decodeJSON(req.Body, obj)
}

感觉很牛!!!🎊🎊🎊

返回体类型

其实一般响应都是用的c.JSON (因为我做的前后端分离的嘿嘿!!!)

核心代码:

// JSON 将给定的结构作为 JSON 序列化到响应正文中。
// It also sets the Content-Type as "application/json".
//c.JSON具体实现函数
func (c *Context) JSON(code int, obj interface{}) {
	c.Render(code, render.JSON{Data: obj})
}
// Render writes the response headers and calls render.Render to render data.
//设置状态码以及将信息写入response里面
func (c *Context) Render(code int, r render.Render) {
    //设置状态码
	c.Status(code)
	//检测状态码
	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}
	//返回体写入信息
	if err := r.Render(c.Writer); err != nil {
		panic(err)
	}
}
//json实现的方法
func (r JSON) Render(w http.ResponseWriter) (err error) {
	if err = WriteJSON(w, r.Data); err != nil {
		panic(err)
	}
	return
}
// WriteJSON 编组给定的接口对象并使用自定义 ContentType 写入它
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
    //sets the Content-Type as "application/json".
	writeContentType(w, jsonContentType)
	jsonBytes, err := json.Marshal(obj)
	if err != nil {
		return err
	}
	_, err = w.Write(jsonBytes)
	return err
}

这些代码最后的结果都是讲json中的data数据写入response中

为什么说是在response结构体里面?

请看博客:cloud.tencent.com/developer/a…

中间件

我们先了解一下中间件的流程是怎么样的!!!

以下解释来自极客兔兔(吹爆):

func A(c *Context) {
    part1
    c.Next()
    part2
}
func B(c *Context) {
    part3
    c.Next()
    part4
}

假设我们应用了中间件 A 和 B,和路由映射的 Handler。c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),接下来的流程是这样的:

  • c.index++,c.index 变为 0
  • 0 < 3,调用 c.handlers[0],即 A
  • 执行 part1,调用 c.Next()
  • c.index++,c.index 变为 1
  • 1 < 3,调用 c.handlers[1],即 B
  • 执行 part3,调用 c.Next()
  • c.index++,c.index 变为 2
  • 2 < 3,调用 c.handlers[2],即Handler
  • Handler 调用完毕,返回到 B 中的 part4,执行 part4
  • part4 执行完毕,返回到 A 中的 part2,执行 part2
  • part2 执行完毕,结束。

一句话说清楚重点,最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2。恰恰满足了我们对中间件的要求,接下来看调用部分的代码,就能全部串起来了。

如果你可以看懂上面的流程就懂了下面的代码的执行逻辑了!!😆😆😆

核心代码:

func (c *Context) Next() {
	c.index++   //移动到下一个HandlerFunc
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)//执行下一个HandlerFunc
		c.index++
	}
}

总结

以上就是gin的Context(上下文)一些常用的函数源码解析。可能你会有点听不懂,欢迎提出来。

吐槽

其实我看完,还是有点不理解。可能是不理解net/http库的一些实现,因为他的很多函数实现参数都是接口,然后找不到具体的实现,即使找到了,感觉又回到了一开始的困境。

感觉go都是面对接口解决。

可能是自己道行不够!!慢慢学吧!