gin.Context初探

最近有使用gin-vue-admin框架来做一个管理后台,笔者注意到获取参数有个这样的方法:c.ShouldBindJSON() ,这样一个获取参数的方法,当然这个方法是针对post方式提交数据,于是比较好奇这个是怎么实现的,分以下两步:

  1. 数据写入Context;

  2. 从Context读取数据;

首先我们写代码的时候会给路由函数带上一个参数(c*gin.Context), 首先这个是个固定的写法,因为gin关于http请求的handler 方法是定义是需要传入这样一个参数,下面看下源码:

//我们在设置我们的路由一般都是这样写的,这里举几个例子
{
        goodsManageRouter.POST("goodsList", goodsManageApi.GetGoodsList)     
        goodsManageRouter.GET("goodsDetail", goodsManageApi.GoodsDetail)   
        goodsManageRouter.POST("addGoods", goodsManageApi.AddGoods) 
    }
//真正处理的函数一般这样写
func(g *GoodsManageApi) GetGoodsList(c *gin.Context){
      params := request.GoodsListReq{}
   // s := c.Query("name")
    _ = c.ShouldBindJSON(¶ms)
    ...
}
//这里举两个例子POST和GET方法,这两个方法实际都是注册路由,其中handlers 这个参数是
//HandlerFunc 类型的
// POST is a shortcut for router.Handle("POST", path, handle).
type HandlerFunc func(*Context)  //表示参数的类型是Context指针类型
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodPost, relativePath, handlers)
}

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

从以上代码我们知道为啥会是带c *gin.Context 参数这种固定写法了。但是我们还是不知道数据是怎么写入到这个c里面的,如果想要了解这些,我们需要从服务(这里是http服务)的运行到执行有个比较清晰的了解,整个过程大致是:监听tcp端口(listen)——>接受连接(accept)——>开启协程处理,下面附上我仔细看了下的源码:

1)net\http\server.go

func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {  //检测服务是否关闭
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)  //监听端口
    if err != nil {
        return err
    }
    return srv.Serve(ln)  //进行处理,并返回数据
}

上面这段基本都看得懂,下面看下真正的处理函数;

2)net\http\server.go

//从监听器上接受即将到来的连接,针对每个连接都会创建一个goroutine去处理
// goroutine会读取请求然后调用handler处理并返回结果
// 当监听器返回的是*tls.Conn的时候才支持HTTP/2,tls是加密连接
func (srv *Server) Serve(l net.Listener) error {
 ...
 ....
 
    var tempDelay time.Duration // how long to sleep on accept failure

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        go c.serve(connCtx)   //开启协程调用方法
    }
}
  1. net\http\server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    ...
    ...

    for { 
        w, err := c.readRequest(ctx)    //循环读取请求
     ...
   ...

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        // But we're not going to implement HTTP pipelining because it
        // was never deployed in the wild and the answer is HTTP/2.
        serverHandler{c.server}.ServeHTTP(w, w.req)   //处理请求代码
        w.cancelCtx()
        if c.hijacked() {
            return
        }
        w.finishRequest()
        if !w.shouldReuseConnection() {
            if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
                c.closeWriteAndWait()
            }
            return
        }
        c.setState(c.rwc, StateIdle, runHooks)
        c.curReq.Store((*response)(nil))

        ...
        ...
    }
}

下面重点看下serverHandler{c.server}.ServeHTTP(w, w.req) 这行代码里面做的事情

4)net\http\server.go

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
....
....
....
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler  //获取Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }

    if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
        var allowQuerySemicolonsInUse int32
        req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
            atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
        }))
        defer func() {
            if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
                sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")
            }
        }()
    }

    handler.ServeHTTP(rw, req)   //处理请求
}

从上面这段代码可以看出ServeHTTP函数最终会调用handler.ServeHTTP()方法,但是handler在不为空或者请求的URI不是*号和请求的方法不是OPTIONS的情况下实际是一个接口类型,接口里面包含了ServeHTTP这个方法,也就是说这个方法的调用实际是和传入的handler参数有关,是由参入参数来实现这个方法的,于是我们需要找到路由注册的地方,也就是初始化服务传入的Handler类型,找到了对应的ServeHTTP方法

5) gin.go

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)  //从对象池里面拿到一个对象,并断言为Context指针类型变量
    c.writermem.reset(w)
    c.Request = req   // 当前请求写入到context 对象中
    c.reset()

    engine.handleHTTPRequest(c)  // 处理请求

    engine.pool.Put(c)
}

6)gin.go

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.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()    //执行调用
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    ...
    ...
}

从上面一段代码可以看出如果当前可以匹配到处理的方法,便会执行value.handlers这段代码,通过调用c.Next()方法执行指定的处理函数,下面来看下c.Next()方法:

7)gin@1.7.0\context.go

// Next()方法只能在中间件中使用
//在正在被调用的handler 里面执行被挂起的handler, 这是翻译的效果,笔者的理解就是在中间件
//中去执行一个handler,执行完后接着中间件下面的代码继续执行

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)  //执行方法,c参数是被赋值了当前请求requst的相关信息
        c.index++
    }
}

到此,我们就解释了文章开头的第一个问题,这里笔者没有对这些代码做详细的分析,就大致看下整个流程。好了下面再来看第二个问题,取数据;

取数据很容易,大致的流程是调用把请求体req.Body解析到指定的对象中,最终解析的代码如下:

func decodeJSON(r io.Reader, obj interface{}) error {
    decoder := json.NewDecoder(r)
    if EnableDecoderUseNumber {
        decoder.UseNumber()   //防止数据被转换成float64
    }
    if EnableDecoderDisallowUnknownFields {
        decoder.DisallowUnknownFields()  //如果目标结构体中不包含传入的某个字段则会返回一个错误
    }
    if err := decoder.Decode(obj); err != nil {
        return err
    }
    return validate(obj)
}

具体是如何解析的这篇文章就不详细解释了,后面笔者会出一篇文章详细解释下decode操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351