以net/http为底层的Gin架构
首先看一下Gin是如何启动一个http web server服务的:
func main() {// Creates a gin router with default middleware:// logger and recovery (crash-free) middlewarerouter := gin.Default()router.GET("/someGet", getting)// By default it serves on :8080 unless a// PORT environment variable was defined.router.Run()// router.Run(":3000") for a hard coded port}
Gin启动一个web server非常简单,一个gin.Default()把默认的配置设置获取,一个GET()把处理函数注册到框架内,一个Run()就可以把将Server绑定指定端口幷跑起来。Gin server最重要的也是上面提到的3个部分。
首先我们先看下gin.Default()做了些什么工作:
// Default returns an Engine instance with the Logger and Recovery middleware already attached.func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine}
我们通过Default接口返回一个启动Gin server的默认配置,即New出来一个egine对象,幷注册了Logger(), Recovery()两个中间件,即默认使用了日志记录,以及利用recovery来捕捉panic,打印出函数调用栈。
我们看一下New一个engine对象时,会初始化哪些成员变量
/ New returns a new blank Engine instance without any middleware attached.// By default the configuration is:// - RedirectTrailingSlash: true// - RedirectFixedPath: false// - HandleMethodNotAllowed: false// - ForwardedByClientIP: true// - UseRawPath: false// - UnescapePathValues: truefunc New() *Engine {debugPrintWARNINGNew()engine := &Engine{RouterGroup: RouterGroup{Handlers: nil,basePath: "/",root: true,},FuncMap: template.FuncMap{},RedirectTrailingSlash: true,RedirectFixedPath: false,HandleMethodNotAllowed: false,ForwardedByClientIP: true,RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},TrustedProxies: []string{"0.0.0.0/0"},TrustedPlatform: defaultPlatform,UseRawPath: false,RemoveExtraSlash: false,UnescapePathValues: true,MaxMultipartMemory: defaultMultipartMemory,trees: make(methodTrees, 0, 9), // 路由树,我们注册的函数都会放在这里,每个节点都记录着注册的函数delims: render.Delims{Left: "{{", Right: "}}"},secureJSONPrefix: "while(1);",}engine.RouterGroup.engine = engineengine.pool.New = func() interface{} { //注意这里,使用了syc.pool来缓存context对象return engine.allocateContext()}return engine}
我们继续看下router.GET(“/someGet”, getting)具体做了哪些工作,GET的作用就是将我们的处理函数注册到指定的URL上,而注册的过程是这样的:GET->handle->combineHandlers
// 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)}func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()}func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {finalSize := len(group.Handlers) + len(handlers)assert1(finalSize < int(abortIndex), "too many handlers")mergedHandlers := make(HandlersChain, finalSize)copy(mergedHandlers, group.Handlers)copy(mergedHandlers[len(group.Handlers):], handlers)return mergedHandlers}func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {assert1(path[0] == '/', "path must begin with '/'")assert1(method != "", "HTTP method can not be empty")assert1(len(handlers) > 0, "there must be at least one handler")debugPrintRoute(method, path, handlers)root := engine.trees.get(method)if root == nil {root = new(node)root.fullPath = "/"engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)// Update maxParamsif paramsCount := countParams(path); paramsCount > engine.maxParams {engine.maxParams = paramsCount}}
GET方法是对group.handle的封装,group.handle方法中:
- group.calculateAbsolutePath 计算得到绝对路径的url
- group.combineHandlers 返回需要执行的handler列表,其中包括中间件+注册到该URI的handler
- engine.addRoute 将handlers跟URL注册绑定到路由树的节点上,方便后续快速搜索访问。
函数combineHandlers中,使用了两次deep copy操作,第一deep copy是为了把已有的中间件handlers拷贝一份到mergedHandlers;第二次deep copy把本次要注册的handler拷贝放置到mergedHandlers后面。这里注意:
两次拷贝是有序的,即中间件handlers在前,新注册的handler在后,如果我们设置了中间件,我们的请求是先走中间件handler,再走GET等方法注册的handler。
我们最后看下Run()函数的处理逻辑:
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.// It is a shortcut for http.ListenAndServe(addr, router)// Note: this method will block the calling goroutine indefinitely unless an error happens.func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()err = engine.parseTrustedProxies()if err != nil {return err}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine)return}
Run方法的本质是使用了net/http中的http.ListenAndServe启动一个http web server来监听端口,注意http.ListenAndServe第二个参数,Gin传入的是engine对象,我们知道http.ListenAndServe要求第二个参数传入的是一个实现了ServeHTTP对象,而Gin的engine也确实实现这个ServeHTTP方法。
// 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 = reqc.reset() // context对象使用前先reset,初始化engine.handleHTTPRequest(c)engine.pool.Put(c) // context用完了,还回给对象池}
这里注意到,context是存储到engine的sync.pool中的,这种做法的好处是复用已经使用过的对象,来达到优化内存使用和回收的目的。因为每处理一个请求都会要求分配一个contex对象,因此分配和回收context对象是个很频繁的操作,十分消耗性能。因此Gin利用sync.pool对象池来优化这个对象分配的操作。一开始这个池子会初始化一些contex对象供Gin使用,如果不够了,自己会通过new产生一些,当你放回去了之后这些对象会被别人进行复用。
这里再仔细分析下Gin的context结构体,这是Gin中最重要的结构体之一,另一个同样重要的是engine结构体。engine结构体负责管理整个Gin web server,而context结构体时管理每一个请求,一个http请求到达服务器,就会有一个新的context产生,这个context因请求的到来而创建,因请求的结束而消亡。
注意Gin自己实现的context跟Go自带的context是不一样的,Go的context常用于做请求的生命周期管理,比如超时控制等;而Gin实现的context用于请求的各个handler间传递数据。
// 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 responseWriterRequest *http.RequestWriter ResponseWriterParams Paramshandlers HandlersChain // 我们注册的处理函数index int8fullPath stringengine *Engineparams *Params// This mutex protect Keys mapmu sync.RWMutex// Keys is a key/value pair exclusively for the context of each request.Keys map[string]interface{} // 用于各个中间件handler之间传递数据,需要使用sync.RWMutex保证读写安全// 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// SameSite allows a server to define a cookie attribute making it impossible for// the browser to send this cookie along with cross-site requests.sameSite http.SameSite}
engine.handleHTTPRequest(c)是处理单个请求的入口,传入参数就是我们从对象池中取出的已初始化了的context对象。
func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.Method // GET/POST/PUT/DELETED等方法rPath := c.Request.URL.Path // URLunescape := falseif engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues}if engine.RemoveExtraSlash {rPath = cleanPath(rPath)}// Find root of the tree for the given HTTP methodt := engine.trees // engine.trees的类型:type methodTrees []methodTree// 至于这里为什么engine.trees是个slice,这是因为slice中每个元素是每个方法(GET/POST等)的treefor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, unescape) // 从对应的节点取出处理函数if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next() // 开始处理handlers列表的所有函数c.writermem.WriteHeaderNow()return}if httpMethod != http.MethodConnect && 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.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)}
看了上面的代码,会产生一个疑问,为什么engine.trees是个slice?为什么会有多个路由树呢?原因是Gin的路由树是每个HTTP方法一棵树,比如GET方法是一棵树,POST方法也是单独一颗树。当一个请求过来了,需要遍历每棵方法树,所以看代码也可以发现,遍历时第一个判断也是判断if t[i].method != httpMethod,不是请求的方法就跳过。而方法树的建立是惰性建立,是在注册GET/POST等方法时就建立的,如果我们没有为某个方法注册handler,那么就不会预先建立对应的树。具体函数是下面的addRoute。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {assert1(path[0] == '/', "path must begin with '/'")assert1(method != "", "HTTP method can not be empty")assert1(len(handlers) > 0, "there must be at least one handler")debugPrintRoute(method, path, handlers)root := engine.trees.get(method)if root == nil {root = new(node)root.fullPath = "/"engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)// Update maxParamsif paramsCount := countParams(path); paramsCount > engine.maxParams {engine.maxParams = paramsCount}}

middleware对请求做前置后置处理
Gin相对于Go自带的http框架一个很大的改进是引入了中间件的功能,也就是说,我们可以在执行我们请求处理函数的前后,可以自定义插入各种函数,这个技术其实就是我们常说的函数狗子hook,在一个处理前后加函数钩子,比如在执行指定函数前,我们先执行A,B,C函数,在执行后会触发执行X,Y,Q函数。
Gin添加中间件是这样的写法:
func TestMiddlewareGeneralCase(t *testing.T) {signature := ""router := New()router.Use(func(c *Context) {signature += "A"c.Next()signature += "B"})router.Use(func(c *Context) {signature += "C"})router.GET("/", func(c *Context) {signature += "D"})router.NoRoute(func(c *Context) {signature += " X "})router.NoMethod(func(c *Context) {signature += " XX "})// RUNw := performRequest(router, "GET", "/")// TESTassert.Equal(t, http.StatusOK, w.Code)assert.Equal(t, "ACDB", signature)}
利用Use方法将中间件处理函数注册到中间件handlers列表里。中间件处理函数是以slice的形式存在,使用Use方法把所有middleware处理函数append到slice中。因此先位于Use前面的中间件是优先执行的。
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {group.Handlers = append(group.Handlers, middleware...)return group.returnObj()}
Next方法的本质是对slice中注册的所有处理函数进行遍历和执行。当一个函数体内,调用Next(),就会执行下一个中间件注册好的handler,Next()后的逻辑会在所有中间件handler都执行完毕后再执行。
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}}
看看这个例子
func Middleware(c *gin.Context) {fmt.Println("Hello Before;")c.Next()fmt.Println("Hello After;")}
- c.Next() 之前的操作是在 Handler 执行之前就执行;
- c.Next() 之后的操作是在 Handler 执行之后再执行;
之前的操作一般用来做验证处理,访问是否允许之类的。
之后的操作一般是用来做总结处理,比如格式化输出、响应结束时间,响应时长计算之类的。
如果使用gin.New来new一个engine,那么我们起的这个server是不带任何中间件的;如果使用的是gin.Default来new一个engine,那默认使用的中间件有Logger和Recovery,用户新加的中间件只会append到Logger和Recovery之后,遍历执行时也是按照Logger、Recovery、Middleware1、Middleware2…注册顺序来执行。因此可以理解为,在处理一个请求时,由于有
Recovery是首先执行的,因此后面的handler即使触发了panic也能被捕捉和recovery,因此不需要担心进程会因为panic而crash。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine}
前缀树路由
net/http web server用了一个非常简单的map结构存储了路由表,使用map存储键值对,key是请求的URI,value是handler处理函数。这种map维护的路由,索引非常高效,当然这个路由map是加锁的,具体代码:https://github.com/golang/go/blob/5f3dabbb79fb3dc8eea9a5050557e9241793dce3/src/net/http/server.go#L2453
但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。那如果我们想支持类似于/hello/:name或者/user*/login这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。
实现动态路由最常用的数据结构,是前缀树(Trie树)。前缀树的特点:每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。Gin采取了前缀树作为路由查找的数据结构,支持了动态路由功能。
HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。

这里Gin的前缀树路由实现没有特别之处,所以大概看下代码就好了。
https://github.com/gin-gonic/gin/blob/3a6f18f32f22d7978bbafdf9b81d3a568b7a5868/tree.go
请求数据binding为对象
请求转结构体的本质调用是github.com/go-playground/validator/v10来做利用validator做json/xml/protobuf到对象的转换。
在请求处理时我很喜欢使用ShouldBind来将http请求body转换为结构体,我们常用的写法如下:
type formA struct {Foo string `json:"foo" xml:"foo" binding:"required"`}type formB struct {Bar string `json:"bar" xml:"bar" binding:"required"`}func SomeHandler(c *gin.Context) {objA := formA{}objB := formB{}// This c.ShouldBind consumes c.Request.Body and it cannot be reused.if errA := c.ShouldBind(&objA); errA == nil {c.String(http.StatusOK, `the body should be formA`)// Always an error is occurred by this because c.Request.Body is EOF now.} else if errB := c.ShouldBind(&objB); errB == nil {c.String(http.StatusOK, `the body should be formB`)} else {...}}func (c *Context) ShouldBind(obj interface{}) error {b := binding.Default(c.Request.Method, c.ContentType()) // 从这里就已经知道需要使用哪个binding方法了(json/xml/form等)return c.ShouldBindWith(obj, b)}func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {return b.Bind(c.Request, obj) // 根据body的协议来选择调用响应的bind方法来解码}
比如请求body的协议是json,那就使用json的bind decoder来将body翻译为结构体。
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {if req == nil || req.Body == nil {return errors.New("invalid request")}return decodeJSON(req.Body, obj)}func decodeJSON(r io.Reader, obj interface{}) error {decoder := json.NewDecoder(r)if EnableDecoderUseNumber {decoder.UseNumber()}if EnableDecoderDisallowUnknownFields {decoder.DisallowUnknownFields()}if err := decoder.Decode(obj); err != nil {return err}return validate(obj) // 这里body转struct的核心步骤,用了validator库}
我们注意到,这个ValidateStruct使用到了反射机制,我们利用reflect.ValueOf和value.Kind()来判断当前的对象的类型,这里处理的类型有三种:指针、结构体和slice/array。
func validate(obj interface{}) error {if Validator == nil {return nil}return Validator.ValidateStruct(obj)}// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.func (v *defaultValidator) ValidateStruct(obj interface{}) error {if obj == nil {return nil}value := reflect.ValueOf(obj)switch value.Kind() {case reflect.Ptr:return v.ValidateStruct(value.Elem().Interface())case reflect.Struct:return v.validateStruct(obj)case reflect.Slice, reflect.Array:count := value.Len()validateRet := make(sliceValidateError, 0)for i := 0; i < count; i++ {if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {validateRet = append(validateRet, err)}}if len(validateRet) == 0 {return nil}return validateRetdefault:return nil}}// validateStruct receives struct typefunc (v *defaultValidator) validateStruct(obj interface{}) error {v.lazyinit()return v.validate.Struct(obj)}func (v *defaultValidator) lazyinit() {v.once.Do(func() {v.validate = validator.New()v.validate.SetTagName("binding")})}
异常处理
Gin默认使用过的中间件有Logger和Recovery,用户新加的中间件只会append到Logger和Recovery之后,遍历执行时也是按照Logger、Recovery、Middleware1、Middleware2…注册顺序来执行。因此可以理解为,在处理一个请求时,由于有
Recovery是首先执行的,因此后面的handler即使触发了panic也能被捕捉和recovery,因此不需要担心进程会因为panic而crash。如果没有特殊的需求,请使用gin.Default()而非gin.New()来获取一个engine对象。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine}
https://github.com/gin-gonic/gin/blob/3a6f18f32f22d7978bbafdf9b81d3a568b7a5868/recovery.go#L51
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.func Recovery() HandlerFunc {return RecoveryWithWriter(DefaultErrorWriter)}// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {var logger *log.Loggerif out != nil {logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)}return func(c *Context) {defer func() {if err := recover(); err != nil {// Check for a broken connection, as it is not really a// condition that warrants a panic stack trace.var brokenPipe boolif ne, ok := err.(*net.OpError); ok {if se, ok := ne.Err.(*os.SyscallError); ok {if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {brokenPipe = true}}}if logger != nil {stack := stack(3)httpRequest, _ := httputil.DumpRequest(c.Request, false)headers := strings.Split(string(httpRequest), "\r\n")for idx, header := range headers {current := strings.Split(header, ":")if current[0] == "Authorization" {headers[idx] = current[0] + ": *"}}headersToStr := strings.Join(headers, "\r\n")if brokenPipe {logger.Printf("%s\n%s%s", err, headersToStr, reset)} else if IsDebugging() {logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",timeFormat(time.Now()), headersToStr, err, stack, reset)} else {logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",timeFormat(time.Now()), err, stack, reset)}}if brokenPipe {// If the connection is dead, we can't write a status to it.c.Error(err.(error)) // nolint: errcheckc.Abort()} else {handle(c, err)}}}()c.Next()}}
recovery中间件的实现简单而言就是recovery语句+Next语句,因为实现了recovery来捕捉panic,因此不需要担心因为panic而到最后程序crash,并且当panic发生时,recovery中间件还把函数调用栈打印出来,方便我们定位问题。
