See how the Gin framework is implemented

Basic use

Gin is a high-performance golang web framework. Its code is actually very few. Compared with spring, understanding gin is really chopping melons and vegetables

Let's take a look at the basic usage of gin

import "github.com/gin-gonic/gin"

func Init() {
	r := gin.Default()
	initRoute(r)
	r.Run()
}

func initRoute(r *gin.Engine) {
	r.GET("ping", handler)
}

func handler(ctx *gin.Context) {
	ctx.String(200, "Hello World!")
}

In this example, we start a simple web service using Gin Default creates an engine object. Engine is the core of Gin core, which will be explained in detail later. Then register a route through initRoute. When you access localhost:8080/ping, you will return Hello World!.

Finally, call r.run to enable the service to start. With a few lines of code, you can start a web service through gin. We mainly explore its implementation principle. We won't elaborate on more language features and usage of gin here. More examples can be found here Gin Examples.

Engine

Explanation on the official website: Engine is an example of the framework, which includes multiplexers, middleware and various configurations.

type Engine struct {
	// Routing group
	RouterGroup

	// If set to true, if / foo / does not match the route, it will automatically redirect to / foo
	RedirectTrailingSlash bool

	// Similar to RedirectTrailingSlash, make some modifications to the path
	RedirectFixedPath bool

	HandleMethodNotAllowed bool

	ForwardedByClientIP bool

	// DEPRECATED
	AppEngine bool

	// Via URL Rawpath to get parameters
	UseRawPath bool

	// If true, the path value will be unescaped.
	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
	// as url.Path gonna be used, which is already unescaped.
	UnescapePathValues bool

	RemoveExtraSlash bool

	// List of headers used to obtain the client IP when
	// `(*gin.Engine).ForwardedByClientIP` is `true` and
	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
	// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
	RemoteIPHeaders []string

	// If set to a constant of value gin.Platform*, trusts the headers set by
	// that platform, for example to determine the client IP
	TrustedPlatform string

	// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}

With so many fields in the Engine, there are still many ways not to post them here. Don't worry. We peel the cocoon bit by bit to uncover the true face of gin

Simply put, a gin service corresponds to an Engine instance. r = gin.Default() actually creates an Engine object, and its source code is not complex

func Default() *Engine {
	engine := New()
	// Registration Middleware
	engine.Use(Logger(), Recovery())
	return engine
}

func New() *Engine {
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		......
	}
	engine.RouterGroup.engine = engine
	// pool stores the context object
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

First, a New Engine comes out, initializes some infrastructure, and then injects two middleware logger s and recovery through the Use method. Their types are HandlerFunc. Loggers are used for logging. Recovery is used to recover when the service panic is recover, and then returns 500 to avoid direct service crash.

Interestingly, there is a field pool in engine, and the type is sync Pool. When creating the engine, we also initialized the pool. About sync Pool can see my other article [todo]. You can see that the New method of pool creates and returns a context, which is an object that carries context information for every request. It can be imagined that the context is very heavy. If each request creates and destroys a context object, the GC will not be able to support it, How to become a high-performance wen server? So Gin through sync Pool to reuse context and optimize GC.

route

No matter how the web service framework is implemented, its most basic function is routing. In short, it parses the url and then dispatches different handler s to process the corresponding requests.

After creating the engine, we also tell it how to handle the routing information. Registering routes is very simple. Engine supports six RESTful methods.

func initRoute(r *gin.Engine) {
	r.GET("ping", pingHandler)
}

In fact, the Engine implements routing processing by inheriting the RouterGroup (which can be found in the structure of the Engine). All routing processing is completed in the RouterGroup. Take the Get source code as an example

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

After registering a route through GET, we will GET an IRoutes object. IRoutes itself is an interface and defines a series of methods, including GET, POST... Etc. it is not difficult to find that RouterGroup implements IRoutes. In addition, when registering a route, not only one processing method can be inserted into a path. The three points in the source code represent that we can pass in multiple handler s, which will be called one by one when the request arrives. GET directly calls the internal group Handle, how is it implemented

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

There are three steps

1. Calculate absolute path

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
	return joinPaths(group.basePath, relativePath)
}

In fact, it is the string join, which is combined with basePath and the currently registered path. Where does basePath come from and why join?

When we create the Engine, we actually register a path, which is "/". All other subsequent registered paths are hung under it. If we regard it as the root node of a tree, there are more child nodes under it, and a complete path is the combination of all paths passing from the root node to the child nodes. In fact, Gin uses the tree structure for routing management, which will be discussed later.

2. Add handlers

The handlers passed in before are path, but other people's basePath also has handlers. basePath is the prefix of path. When you initiate a request, should all basePath handlers be reviewed. The addition process is not responsible, that is, reopen an array space, copy both basePath and path handlers here, and then return the array address.

3. Associate absolute paths and handlers

This step is more important and the logic is more complex. First look at the outermost code and filter out some irrelevant code

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	......
	
	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)
	
	......
}

First, get the root node according to the method. What are the methods? GET, POST, PUT, DELETE…

If the root node does not exist, create one and join the entine In trees, entine Trees are methodTrees. The definition of methodTrees is [] methodTree, so entity Trees is an array of methedTree. What's a metedtree?

type methodTree struct {
	method string
	root   *node
}

type node struct {
	path      string
	indices   string
	wildChild bool
	nType     nodeType
	priority  uint32
	children  []*node // child nodes, at most 1 :param style node at the end of the array
	handlers  HandlersChain
	fullPath  string
}

The link is a little long. Draw a picture more clearly

To be continued

Keywords: Go Web Development Back-end

Added by martin_g on Fri, 14 Jan 2022 11:29:42 +0200