一、基础知识

Context之间的关系,是一颗树形结构,父Context消亡后,子Context会自动消亡。

准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

main.go 就是这颗树的根Context节点,称之为background.

/ A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}

func Background() Context {
   return background
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 
func WithValue(parent Context, key, val interface{}) Context

1. Gin - Context 上下文 牛逼的Context

Gin-Context 实现了对request和response的封装,是Gin的核心实现之一,学习使用gin框架就是学习使用Context包的过程。内部封装了request 和response 过程中的数据。

作用:
CONTEXT CREATION  reset() , Copy(), HandlerName
FLOW CONTROL  Next, IsAbort(), Abort(),AbortWithStatus(code int)
ERROR MANAGEMENT  Error(err error)
METADATA MANAGEMENT  
Set(key string, value interface{}),Get(key string) (value interface{}, exists bool)

源码结构:

// 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.
type Context struct {
   writermem responseWriter
   Request   *http.Request
   Writer    ResponseWriter

   Params   Params
   handlers HandlersChain
   index    int8
   fullPath string

   engine *Engine

   // This mutex protect Keys map
   KeysMutex *sync.RWMutex

   // 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()
   queryCache url.Values

   // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
   // or PUT body parameters.
   formCache url.Values
}

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
   c.index++
   for c.index < int8(len(c.handlers)) {
      c.handlers[c.index](c)
      c.index++
   }
}

机制:Pool 里装的对象可以被无通知地被回收.

sync.Poolsync.Pool
NewPoolGet()Put()

3. RouterGroup

其中handler方法的第一参数必须为Context类型

  // RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
   Handlers HandlersChain  
   basePath string
   engine   *Engine
   root     bool
}

添加中间件本质上注册公用 handler
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
   group.Handlers = append(group.Handlers, middleware...)
   return group.returnObj()
}

// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{
      Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
      engine:   group.engine,
   }
}

// POST is a shortcut for router.Handle("POST", path, handle).
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)
}

4. Engine

Engine代表的就是gin框架本身实例 ;

Engine实现了一个ServeHTTP的方法, 是 Gin 框架核心中的核心:

// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
   RouterGroup
   pool             sync.Pool
   trees            methodTrees
   ...
}

// 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)
}

Engine 与 RouterGroup 之间存在双向指针,实现了类似友元的关系,路由的添加和注册可以直接使用Engine的实例进行添加注册,实际的实现仍然是RouterGroup内进行添加和注册。

二、框架启动过程

1. New 一个Engine实例

app := gin.Default()

2. 注册添加路由、中间件

你可能会疑惑,为什么这里的路由处理函数要接受一个 gin.Context 类型的参数,是在何时传入的?

Engine结构体本身发挥的核心功能就是路由处理。

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

3. 启动Gin框架

Run 本质就是将 注册的路由信息engine 绑定到一个 http.Server,然后开始开始监听并处理请求

app.Run()

func (engine *Engine) Run(addr ...string) (err error) {
   defer func() { debugPrintError(err) }()

   address := resolveAddress(addr)
   debugPrint("Listening and serving HTTP on %s\n", address)
   err = http.ListenAndServe(address, engine)
   return
}

进入到 golang net/http包 (以下不属于gin框架) ,Hanlder 是一个实现了ServeHTTP方法的类型

func ListenAndServe(addr string, handler Handler) error {
   server := &Server{Addr: addr, Handler: handler}
   return server.ListenAndServe()
}

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)
}


func (srv *Server) Serve(l net.Listener) error {
   if fn := testHookServerServe; fn != nil {
      fn(srv, l) // call hook with unwrapped listener
   }

   origListener := l
   l = &onceCloseListener{Listener: l}
   defer l.Close()

   if err := srv.setupHTTP2_Serve(); err != nil {
      return err
   }

   if !srv.trackListener(&l, true) {
      return ErrServerClosed
   }
   defer srv.trackListener(&l, false)

   baseCtx := context.Background() 
   if srv.BaseContext != nil {
      baseCtx = srv.BaseContext(origListener)
      if baseCtx == nil {
         panic("BaseContext returned a nil context")
      }
   }

   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) // 将此上下文,与连接 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) // 处理新的连接 
   }
}

Background() 为 后台主上下文,可以理解为上下文的根节点。

三、API调用主过程

当监听到请求时,gin框架就会衍生一个Context,为它添加上请求相关参数。开一个go程进行处理。

1. net\http 创建连接,握手

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
   c.remoteAddr = c.rwc.RemoteAddr().String()
   ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
   defer func() {
      if err := recover(); err != nil && err != ErrAbortHandler {
         const size = 64 << 10
         buf := make([]byte, size)
         buf = buf[:runtime.Stack(buf, false)]
         c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
      }
      if !c.hijacked() {
         c.close()
         c.setState(c.rwc, StateClosed, runHooks)
      }
   }()

   if tlsConn, ok := c.rwc.(*tls.Conn); ok {
      if d := c.server.ReadTimeout; d != 0 {
         c.rwc.SetReadDeadline(time.Now().Add(d))
      }
      if d := c.server.WriteTimeout; d != 0 {
         c.rwc.SetWriteDeadline(time.Now().Add(d))
      }
      if err := tlsConn.Handshake(); err != nil {
         // If the handshake failed due to the client not speaking
         // TLS, assume they're speaking plaintext HTTP and write a
         // 400 response on the TLS conn's underlying net.Conn.
         if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
            io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
            re.Conn.Close()
            return
         }
         c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
         return
      }
      c.tlsState = new(tls.ConnectionState)
      *c.tlsState = tlsConn.ConnectionState()
      if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
         if fn := c.server.TLSNextProto[proto]; fn != nil {
            h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
            // Mark freshly created HTTP/2 as active and prevent any server state hooks
            // from being run on these connections. This prevents closeIdleConns from
            // closing such connections. See issue https://golang.org/issue/39776.
            c.setState(c.rwc, StateActive, skipHooks)
            fn(c.server, tlsConn, h)
         }
         return
      }
   }

   // HTTP/1.x from here on.

   ctx, cancelCtx := context.WithCancel(ctx)
   c.cancelCtx = cancelCtx
   defer cancelCtx()

   c.r = &connReader{conn: c}
   c.bufr = newBufioReader(c.r)
   c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

   for {
      w, err := c.readRequest(ctx)
      if c.r.remain != c.server.initialReadLimitSize() {
         // If we read any bytes off the wire, we're active.
         c.setState(c.rwc, StateActive, runHooks)
      }
      if err != nil {
         const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

         switch {
         case err == errTooLarge:
            // Their HTTP client may or may not be
            // able to read this if we're
            // responding to them and hanging up
            // while they're still writing their
            // request. Undefined behavior.
            const publicErr = "431 Request Header Fields Too Large"
            fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
            c.closeWriteAndWait()
            return

         case isUnsupportedTEError(err):
            // Respond as per RFC 7230 Section 3.3.1 which says,
            //      A server that receives a request message with a
            //      transfer coding it does not understand SHOULD
            //      respond with 501 (Unimplemented).
            code := StatusNotImplemented

            // We purposefully aren't echoing back the transfer-encoding's value,
            // so as to mitigate the risk of cross side scripting by an attacker.
            fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
            return

         case isCommonNetReadError(err):
            return // don't reply

         default:
            if v, ok := err.(statusError); ok {
               fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
               return
            }
            publicErr := "400 Bad Request"
            fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
            return
         }
      }

      // Expect 100 Continue support
      req := w.req
      if req.expectsContinue() {
         if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
            // Wrap the Body reader with one that replies on the connection
            req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
            w.canWriteContinue.setTrue()
         }
      } else if req.Header.get("Expect") != "" {
         w.sendExpectationFailed()
         return
      }

      c.curReq.Store(w)

      if requestBodyRemains(req.Body) {
         registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
      } else {
         w.conn.r.startBackgroundRead()
      }

      // 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))

      if !w.conn.server.doKeepAlives() {
         // We're in shutdown mode. We might've replied
         // to the user without "Connection: close" and
         // they might think they can send another
         // request, but such is life with HTTP/1.1.
         return
      }

      if d := c.server.idleTimeout(); d != 0 {
         c.rwc.SetReadDeadline(time.Now().Add(d))
         if _, err := c.bufr.Peek(4); err != nil {
            return
         }
      }
      c.rwc.SetReadDeadline(time.Time{})
   }
}

2. gin 框架处理请求核心:ServeHTTP

整个gin框架做的事情,基本上都在这个函数里了

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)
}

3. gin框架进行路由解析

func (engine *Engine) handleHTTPRequest(c *Context) {
   httpMethod := c.Request.Method
   rPath := c.Request.URL.Path
   unescape := false
   if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
      rPath = c.Request.URL.RawPath
      unescape = engine.UnescapePathValues
   }

   if engine.RemoveExtraSlash {
      rPath = cleanPath(rPath)
   }

   // 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
      }
      if httpMethod != "CONNECT" && rPath != "/" {
         if value.tsr && engine.RedirectTrailingSlash {
            redirectTrailingSlash(c)
            return
         }
         if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
         }
      }
      break
   }

   if engine.HandleMethodNotAllowed {
      for _, tree := range engine.trees {
         if tree.method == httpMethod {
            continue
         }
         if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
            c.handlers = engine.allNoMethod
            serveError(c, http.StatusMethodNotAllowed, default405Body)
            return
         }
      }
   }
   c.handlers = engine.allNoRoute
   serveError(c, http.StatusNotFound, default404Body)
}

4. Context 内 执行API Call 即路由handlers

handlers包括中间件,注册的路由handler函数

这里就说明了为什么路由handler的参数必须是Context类型,注册路由处理函数的context就是在这里传入的。

包括中间件,也必须接收一个Context类型的参数

func (c *Context) Next() {
   c.index++
   for c.index < int8(len(c.handlers)) { 
      c.handlers[c.index](c) 
      c.index++
   }
}

5. 执行用户定义的处理函数

业务开发的本质是解析Context,处理其中的参数

学习使用gin框架就是学习使用Context包的过程。

四 、总结

文档资料
官方文档:https://gin-gonic.com/docs/
源码地址:https://github.com/gin-gonic/gin
中文教程:https://learnku.com/docs/gin-gonic/2019
功能特性
高性能:基于RadixTree的路由策略,没有使用反射,占用内存也小;上下文Context使用了sync.pool对象池
中间件:提供简单的中间件注册来实现扩展性,请求被一串链式中间件处理后才应答
路由分组:通过路由group,提供方便和全面的http路由组织
参数获取:提供了包括GET/POST/BIND等便捷的获取参数方法

五、QA

1. 返回数据是如何读取的?

// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
   writeContentType(w, jsonContentType)
   jsonBytes, err := json.Marshal(obj)
   if err != nil {
      return err
   }
   _, err = w.Write(jsonBytes)
   return err
}

2. gin框架为什么不把Context定义为全局变量?


“足球模式”

3. 如何脱离gin框架,手写一个极简http服务器,并完成一个ping接口?

A. 定义一个结构体,实现ServeHTTP方法

ServeHTTP(w http.ResponseWriter, r *http.Request)

B. 将结构体作为参数传入http.ListenAndServe

六、附录

1. 精简版的gin框架

package main

import (
    "fmt"
    "net/http"
    "log"
)

type testEngine struct {

}

## 精简版的 gin框架实现
func (t testEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()       
    fmt.Println(r.Form)
         
    fmt.Println("path", r.URL.Path)
    // 路由分发
    if r.URL.Path == "/ping" {
        fmt.Fprintf(w, "pong!") // 这个写入到 w 的是输出到客户端的
        
    } else {
        fmt.Fprintf(w, "404 not Found!")
    }
}

func main() {
    t := &testEngine{}
    err := http.ListenAndServe(":9090", t) 
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

2. 推荐阅读

深入理解go channel

深入理解GMP 模型

三色标记GC

Go并发编程

内存分配

golang 各种轮子,设计模式的实现