gin的上下文
为什么要设计Context呢?
- 对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",
})
- 针对使用场景,封装
*http.Request和http.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信息
- 存储
WriterResponseWriter相关接口
获取参数
常用的获取参数函数
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这个切片里面,然后从中取值。
至于他怎么去进行分解,等讲到路由再说!
其实,我写项目一般都是用的是
ShouldBindShouldBindJSON
会代替上面一部分的函数。
其实这些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结构体里面?
中间件
我们先了解一下中间件的流程是怎么样的!!!
以下解释来自极客兔兔(吹爆):
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都是面对接口解决。
可能是自己道行不够!!慢慢学吧!