美文网首页Golang
gin源码研究&整理之Context类

gin源码研究&整理之Context类

作者: 星落纷纷 | 来源:发表于2019-05-11 03:57 被阅读0次

    以下研究内容源于1.4.0版本源码

    Context扮演的角色

    每个HTTP请求都会包含一个Context对象,Context应贯穿整个HTTP请求,包含所有上下文信息

    Context对象池

    为了减少GC,gin使用了对象池管理Context

    //gin.go
    // ServeHTTP conforms to the http.Handler interface.
    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
        c := engine.pool.Get().(*Context)
        c.writermem.reset(w)
        c.Request = req
        c.reset()
    
        engine.handleHTTPRequest(c)
    
        engine.pool.Put(c)
    }
    

    Context在goroutine中的并发安全

    gin没有对Context并发安全的处理,应避免多goroutine同时访问同一个Context。如可能存在goroutine同时访问Context的情况,应用事先用Copy方法进行拷贝,如下:

    // Copy returns a copy of the current context that can be safely used outside the request's scope.
    // This has to be used when the context has to be passed to a goroutine.
    func (c *Context) Copy() *Context {
        var cp = *c
        cp.writermem.ResponseWriter = nil
        cp.Writer = &cp.writermem
        cp.index = abortIndex
        cp.handlers = nil
        cp.Keys = map[string]interface{}{}
        for k, v := range c.Keys {
            cp.Keys[k] = v
        }
        return &cp
    }
    

    可以看到拷贝之后,ResponseWriter其实是一个空的对象,所以说,即使拷贝了,也要在主Context中才能返回响应结果。
    这样设计是好的,如果在Context中处理了并发安全,会代码降低执行效率不说,使用者滥用goroutine的话,响应流程就处理混乱了。
    整理后决定连Copy方法也删了。

    Context之Bind

    Bind、ShouldBind相关方法用于请求参数绑定,区别是Bind绑定过程中出现error会直接返回HTTP异常码。

    关于ShouldBindBodyWith

    // ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
    // body into the context, and reuse when it is called again.
    //
    // NOTE: This method reads the body before binding. So you should use
    // ShouldBindWith for better performance if you need to call only once.
    func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
        var body []byte
        if cb, ok := c.Get(BodyBytesKey); ok {
            if cbb, ok := cb.([]byte); ok {
                body = cbb
            }
        }
        if body == nil {
            body, err = ioutil.ReadAll(c.Request.Body)
            if err != nil {
                return err
            }
            c.Set(BodyBytesKey, body)
        }
        return bb.BindBody(body, obj)
    }
    

    这个方法没有用到,作用是先把Body备份一份到Context,下次数据绑定直接从Context中取。没有意义,重新解析一次和直接用Bind没有区别,删掉。

    Context之Negotiate

    设计的初衷是根据客户端请求,返回客户端需要的数据格式,如果不能提供,就返回默认格式

    // Negotiate contains all negotiations data.
    type Negotiate struct {
        Offered  []string
        HTMLName string
        HTMLData interface{}
        JSONData interface{}
        XMLData  interface{}
        Data     interface{}
    }
    

    特殊场景会用到,实际不如直接switch来得快。其实支持多返回结果,实际用的也是单单Data字段,否则就要在内存中生成多份数据,影响效率。比如同时支持JSON和XML,Negotiate里就要同时包含JSONData和XMLData,实际上只包含一个Data就可以了。这里是过度设计,可删除。

    Context之响应(以json格式为例)

    // JSON serializes the given struct as JSON into the response body.
    // It also sets the Content-Type as "application/json".
    func (c *Context) JSON(code int, obj interface{}) {
        c.Render(code, render.JSON{Data: obj})
    }
    
    // AbortWithStatusJSON calls `Abort()` and then `JSON` internally.
    // This method stops the chain, writes the status code and return a JSON body.
    // It also sets the Content-Type as "application/json".
    func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
        c.Abort()//停止下一个路由方法的访问,返回当前写入的请求结果。这行代码放在下行代码后结果是一样的
        c.JSON(code, jsonObj)
    }
    

    如上,只有调用Abort()之后,HTTP请求才会马上返回响应结果,否则,会执行下一个路由方法。
    既然都传入http返回状态码了,常规情况就应该是直接Abort()。而且正常返回流程HTTP状态码就是200。

    而且一个有意思的情况是,如果你这样调用

    c.JSON(200,...)
    c.JSON(200,...)
    c.Abort()
    

    会打印一个[重复写入HTTP状态码]的警告:[WARNING] Headers were already written. Wanted to override status code 我们来看警告的源码

    func (w *responseWriter) WriteHeader(code int) {
        if code > 0 && w.status != code {
            if w.Written() {
                debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
            }
            w.status = code
        }
    }
    

    然后再看gin自带的logger里做了这种事情

    //logger.go 169行
    // ErrorLoggerT returns a handlerfunc for a given error type.
    func ErrorLoggerT(typ ErrorType) HandlerFunc {
        return func(c *Context) {
            c.Next()
            errors := c.Errors.ByType(typ)
            if len(errors) > 0 {
                c.JSON(-1, errors)
            }
        }
    }
    

    惊不惊喜,意不意外?也就是说用到gin自带的logger的时候,还可能给你带来个彩蛋。可能会返回这样的数据给前端:[一串JSON的错误信息]+[正常返回数据]。

    c.JSON(-1, errors)//这里因为code<0,不会引发[WARNING] Headers were already written的后台错误
    c.JSON(200,gin.H{"code":500,"message":"用户名不能为空"})
    c.Abort()
    //这里因为连续两次写入JSON数据,前端收到HTTP状态码是200,但是无法识别正常数据。
    

    程序设计应避免这种模棱两可的情况。还有自带logger最好不用吧、想办法清理掉换上自己的日志库。

    所以说,考虑通常情况,简化调用流程,改良后代码:

    func (c *Context) JSON(obj interface{}) {
        c.Abort()
        c.JSONWithStatus(http.StatusOK, jsonObj)
    }
    
    func (c *Context) JSONWithStatus(code int, jsonObj interface{}) {
        c.Render(code, render.JSON{Data: obj})
        c.Abort()
    }
    

    相比原来舒服多了。这样就够了吗?还不够。因为除了JSON还有好多种数据格式返回,那样每种数据格式,就要开放两个方法。然后继续研究代码。

    如下,发现最后面都会调用到此方法,这个方法还是public的

    // Status sets the HTTP response code.(设置HTTP返回状态码)
    func (c *Context) Status(code int) {
        c.writermem.WriteHeader(code)
    }
    

    那如果不设置的话,默认状态码是多少呢?没错,下面这个defaultStatus就是200

    func (w *responseWriter) reset(writer http.ResponseWriter) {
        w.ResponseWriter = writer
        w.size = noWritten
        w.status = defaultStatus
    }
    

    那么就好办了,只保留一个方法即可

    func (c *Context) JSON(obj interface{}) {
        c.Render(c.writermem.status, render.JSON{Data: obj})
        c.Abort()
    }
    
    //调用示例1--常规返回(200)
    c.JSON("{}")
    //调用示例2--指定状态码返回
    c.Status(500)
    c.JSON("{}")
    

    总结

    1.gin使用对象池高效地管理Context。
    2.gin的Context不是并发安全的,应注意避免。
    3.Bind、ShouldBind相关方法用于请求参数绑定,区别是Bind绑定过程中出现error会直接返回HTTP异常码。
    4.Negotiate为过度设计,可删除。
    5.Context的响应方法可以加上Abort和默认HTTP状态码,用得更舒服点。还能避免踩坑。

    另附一份修改过的context.go文件的代码代码链接

    相关文章

      网友评论

        本文标题:gin源码研究&整理之Context类

        本文链接:https://www.haomeiwen.com/subject/oiltaqtx.html