Gin source code analysis 1: Engine Engine

Original link: http://youngxhui.top/2023/06/gin%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%80/

The HTTP standard library explains how go’s standard library handles requests, but through the analysis of the source code, it can be found that the processing of this part of the standard library is relatively simple, for example, it does not support the parameters carried in the url. However, a large number of frameworks have appeared in the community to supplement the original http.

Most of Go’s HTTP frameworks are rewriting the routing part, which has achieved faster and more accurate route lookup, reduced the time consumed in the route parsing process, and improved the processing speed of the framework.

Standard Library HTTP Handling

Let’s first review the HTTP processing flow of the standard library.

  1. Start the HTTP server: Use the http.ListenAndServe or http.ListenAndServeTLS function to start the HTTP server.

  2. Handling the request: When the server receives an HTTP request, it uses the http.Handler implementation corresponding to the path to handle the request.

  3. Call handler: The server will call ServeHTTP method and pass request-related information as parameters to the method.

  4. Route matching: In ServeHTTP method, a route matching the request is found by comparing the requested path with the registered routes.

  5. Call handler: If a matching route is found, the handler associated with that route is called.

  6. Write response: The processing function writes response data through the ResponseWriter interface to return to the client.

According to the above processing method, there are currently two functions that need to be paid attention to: ServeHTTP and ResponseWriter . These are the two main approaches.

http.ListenAndServe and http.ListenAndServeTLS are used to start the HTTP server; and http.ResponseWriter and http.Request are used to write the response and process the request respectively.

At the same time, through source code analysis, if the handler is not passed in when listening, then the default handler of http, which is ServeMux , will be used, so as long as we implement a Handler according to the standard, the original processing logic will be replaced, and the Handler’s The implementation is also very simple, as long as the interface ServeHTTP is implemented.

gin start

Use gin to implement the simplest ping/pong service. Here is the example provided by the official README .

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
 package main  
import ( "net/http"  
"github.com/gin-gonic/gin" )  
func main () { r := gin . Default () r . GET ( "/ping" , func ( c * gin . Context ) { c . JSON ( http . StatusOK , gin . H { "message" : "pong" , }) }) r . Run () // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") }

The implementation in the example is also very simple, first generate an engine through gin.Default() . Then add related processing methods, and finally start with r.Run() . The following will analyze the gin framework from the very beginning.

Engine

Default is the first function used by gin. This function looks very simple, a total of five lines of code.

 1 2 3 4 5 6
 func Default () * Engine { debugPrintWARNINGDefault () engine := New () engine . Use ( Logger (), Recovery ()) return engine }

Engine structure is mainly initialized through New() function. In gin, it is managed through the structure of Engine , which actually implements the method of ServeHTTP .

Here, the function of Engine is exactly the same as that of ServeMux in the standard library, so its main function is to save the registered handler, and search and call it when it is used. Then let’s take a look at how routes and handlers are registered.

register route

In the above example, routes and handlers are registered via r.GET() method. From the source code, it can be found that not only GET method, but also other request methods, all of which directly call group.handler method.

 1 2 3 4 5 6 7 8 9
 func ( group * RouterGroup ) handle ( httpMethod , relativePath string , handlers HandlersChain ) IRoutes { // 计算绝对路径absolutePath := group . calculateAbsolutePath ( relativePath ) // 合并handler handlers = group . combineHandlers ( handlers ) // 添加相关路由group . engine . addRoute ( httpMethod , absolutePath , handlers ) return group . returnObj () }

You can see from the code that this method is actually relatively simple. From the naming of the code, the following operations are basically performed: first calculate the absolute path, then merge the handlers, and finally add the route.

Why combine

In combineHandlers method, first calculate the length of the current handlers , and judge whether it exceeds the maximum length (the current maximum length is 63). If the maximum length is exceeded, a panic exception will be raised. Here, the source code uses two copy operations. The first time is to copy the data in group.Handlers to mergedHandlers , and the second time is to pass the data of handlers into mergedHandlers .

 1 2 3 4 5 6 7 8
 func ( group * RouterGroup ) combineHandlers ( handlers HandlersChain ) HandlersChain { finalSize := len ( group . Handlers ) + len ( handlers ) // ... mergedHandlers := make ( HandlersChain , finalSize ) copy ( mergedHandlers , group . Handlers ) copy ( mergedHandlers [ len ( group . Handlers ):], handlers ) // ... }

In this way, there are two parts of data in mergeHandler . handlers can be seen from the source code as handlers registered in the project. So what is in group.Handlers ?

In fact, it is the middleware registered when the project starts. In Default method in Engine, we can see that engine.Use(Logger(), Recovery()) project registered two middlewares during initialization, and this Use method is actually to add the middlewares to the above group.Handlers In group.Handlers , I won’t go into details here, but simply explain it. The specific middleware process will be described in the middleware (Middleware) chapter.

To be precise, the purpose of mergeHandlers is to merge middleware and user-defined processing functions into a processing function slice, and register them in the route in a certain order.

add route

The route adding method is also relatively simple, and the core code is only 6 lines in total.

 1 2 3 4 5 6 7
 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 of the tree through the request method, if the root node does not exist, create one, and finally add the relevant routes and handlers. How to add routes here, the route data structure will be explained in the Router chapter.

Up to now, the registration of routes has been completed.

how to run

After understanding how to start an http service in the net/http package of go in detail, in fact, looking back at gin now, everything has become very simple.

In the Run method of gin, it is mainly started by http.ListenAndServe method of the standard library, and this method has been analyzed in detail in the HTTP standard library . The remaining method processes are basically the same as those in the standard library, except for the different One point is to replace the original default Handler with gin.Handler.

As I said before, if you want to become a Handler, you only need to implement ServeHTTP method, and the gin engine implements this method.

According to the previous processing flow of http, when gin receives relevant requests, it will call ServeHTTP method uniformly. This method will process the received parameters, such as finding a suitable handler (Handler), and finally return the unified processing results.

 1 2 3 4 5 6 7 8 9
 // 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 ) }

The first variable used is pool . The sync.Pool type used by the pool here is mainly used to reuse Context . Here, the Context is directly taken out from the pool, and some parameters of the Context are set, and finally engine.handleHTTPRequest method is called.

This is also commonly used

engine.handleHTTPRequest This method mainly handles the user’s HTTP request and determines the processing method of the request. In a nutshell: First, get the HTTP method (such as GET or POST) and URL path of the request, and decode the path if needed. It then searches the processing tree that matches the request. If it finds a matching node, it assigns the handler to the request’s context (c), and writes the HTTP response header. If no matching node is found, a “405 Method Not Allowed” or “404 Not Found” error response is written via serverError .

This is basically the processing of a simple http request. You can see in the code that many things are actually handled by the context (Context).

This article is reproduced from: http://youngxhui.top/2023/06/gin%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%80/
This site is only for collection, and the copyright belongs to the original author.