DEV Community

Cover image for A Deep Dive into Gin: Golang's Leading Framework
Leapcell
Leapcell

Posted on

A Deep Dive into Gin: Golang's Leading Framework

Image description

Introduction

Image description

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API, but with performance up to 40 times faster than Martini. If you need smashing performance, get yourself some Gin.

The official website of Gin introduces itself as a web framework with "high performance" and "good productivity". It also mentions two other libraries. The first one is Martini, which is also a web framework and has a name of a liquor. Gin says it uses its API, but is 40 times faster. Using httprouter is an important reason why it can be 40 times faster than Martini.
Among the "Features" on the official website, eight key features are listed, and we will gradually see the implementation of these features later.

  • Fast
  • Middleware support
  • Crash-free
  • JSON validation
  • Routes grouping
  • Error management
  • Rendering built-in/Extendable

Start with a Small Example

Let's look at the smallest example given in the official documentation.

package main

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

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
Enter fullscreen mode Exit fullscreen mode

Run this example, and then use a browser to visit http://localhost:8080/ping, and you will get a "pong".
This example is very simple. It can be split into only three steps:

  1. Use gin.Default() to create an Engine object with default configurations.
  2. Register a callback function for the "/ping" address in the GET method of the Engine. This function will return a "pong".
  3. Start the Engine to start listening to the port and providing services.

HTTP Method

From the GET method in the above small example, we can see that in Gin, the processing methods of HTTP methods need to be registered using the corresponding functions with the same names.
There are nine HTTP methods, and the four most commonly used ones are GET, POST, PUT, and DELETE, which correspond to the four functions of querying, inserting, updating, and deleting respectively. It should be noted that Gin also provides the Any interface, which can directly bind all HTTP method processing methods to one address.
The returned result generally contains two or three parts. The code and message are always there, and data is generally used to represent additional data. If there is no additional data to return, it can be omitted. In the example, 200 is the value of the code field, and "pong" is the value of the message field.

Create an Engine Variable

In the above example, gin.Default() was used to create the Engine. However, this function is a wrapper for New. In fact, the Engine is created through the New interface.

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            //... Initialize the fields of RouterGroup
        },
        //... Initialize the remaining fields
    }
    engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
Enter fullscreen mode Exit fullscreen mode

Just take a brief look at the creation process for now, and don't focus on the meanings of various member variables in the Engine structure. It can be seen that in addition to creating and initializing an engine variable of type Engine, New also sets engine.pool.New to an anonymous function that calls engine.allocateContext(). The function of this function will be discussed later.

Register Route Callback Functions

There is an embedded struct RouterGroup in the Engine. The interfaces related to HTTP methods of the Engine are all inherited from RouterGroup. The "Routes grouping" in the feature points mentioned on the official website is achieved through the RouterGroup struct.

type RouterGroup struct {
    Handlers    HandlersChain // Processing functions of the group itself
    basePath    string        // Associated base path
    engine      *Engine       // Save the associated engine object
    root        bool          // root flag, only the one created by default in Engine is true
}
Enter fullscreen mode Exit fullscreen mode

Each RouterGroup is associated with a base path basePath. The basePath of the RouterGroup embedded in the Engine is "/".
There is also a set of processing functions Handlers. All requests under the paths associated with this group will additionally execute the processing functions of this group, which are mainly used for middleware calls. Handlers is nil when the Engine is created, and a set of functions can be imported through the Use method. We will see this usage later.

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()
}
Enter fullscreen mode Exit fullscreen mode

The handle method of RouterGroup is the final entry for registering all HTTP method callback functions. The GET method and other methods related to HTTP methods called in the initial example are just wrappers for the handle method.
The handle method will calculate the absolute path according to the basePath of the RouterGroup and the relative path parameter, and at the same time call the combineHandlers method to get the final handlers array. These results are passed as parameters to the addRoute method of the Engine to register the processing functions.

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
}
Enter fullscreen mode Exit fullscreen mode

What the combineHandlers method does is to create a slice mergedHandlers, then copy the Handlers of the RouterGroup itself into it, then copy the handlers of the parameters into it, and finally return mergedHandlers. That is to say, when registering any method using handle, the actual result includes the Handlers of the RouterGroup itself.

Use Radix Tree to Accelerate Route Retrieval

In the "Fast" feature point mentioned on the official website, it is mentioned that the routing of network requests is implemented based on the radix tree (Radix Tree). This part is not implemented by Gin, but by httprouter mentioned in the introduction of Gin at the beginning. Gin uses httprouter to achieve this part of the function. The implementation of the radix tree will not be mentioned here for the time being. We will only focus on its usage for now. Maybe we will write a separate article about the implementation of the radix tree later.
In the Engine, there is a trees variable, which is a slice of the methodTree structure. It is this variable that holds references to all radix trees.

type methodTree struct {
    method string // Name of the method
    root   *node  // Pointer to the root node of the linked list
}
Enter fullscreen mode Exit fullscreen mode

The Engine maintains a radix tree for each HTTP method. The root node of this tree and the name of the method are saved together in a methodTree variable, and all methodTree variables are in trees.

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    //... Omit some code
    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)
    //... Omit some code
}
Enter fullscreen mode Exit fullscreen mode

It can be seen that in the addRoute method of the Engine, it will first use the get method of trees to get the root node of the radix tree corresponding to the method. If the root node of the radix tree is not obtained, it means that no method has been registered for this method before, and a tree node will be created as the root node of the tree and added to trees.
After getting the root node, use the addRoute method of the root node to register a set of processing functions handlers for the path path. This step is to create a node for path and handlers and store it in the radix tree. If you try to register an already registered address, addRoute will directly throw a panic error.
When processing an HTTP request, it is necessary to find the value of the corresponding node through the path. The root node has a getValue method responsible for handling the query operation. We will mention this when talking about Gin processing HTTP requests.

Import Middleware Processing Functions

The Use method of RouterGroup can import a set of middleware processing functions. The "Middleware support" in the feature points mentioned on the official website is achieved through the Use method.
In the initial example, when creating the Engine struct variable, New was not used, but Default was used. Let's take a look at what Default does extra.

func Default() *Engine {
    debugPrintWARNINGDefault()       // Output log
    engine := New()                  // Create object
    engine.Use(Logger(), Recovery()) // Import middleware processing functions
    return engine
}
Enter fullscreen mode Exit fullscreen mode

It can be seen that it is a very simple function. In addition to calling New to create the Engine object, it only calls Use to import the return values of two middleware functions, Logger and Recovery. The return value of Logger is a function for logging, and the return value of Recovery is a function for handling panic. We will skip this for now and look at these two functions later.
Although the Engine embeds RouterGroup, it also implements the Use method, but it is just a call to the Use method of RouterGroup and some auxiliary operations.

func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}
Enter fullscreen mode Exit fullscreen mode

It can be seen that the Use method of RouterGroup is also very simple. It just adds the middleware processing functions of the parameters to its own Handlers through append.

Start Running

In the small example, the last step is to call the Run method of the Engine without parameters. After the call, the entire framework starts running, and visiting the registered address with a browser can correctly trigger the callback.

func (engine *Engine) Run(addr...string) (err error) {
    //... Omit some code
    address := resolveAddress(addr) // Parse the address, the default address is 0.0.0.0:8080
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}
Enter fullscreen mode Exit fullscreen mode

The Run method only does two things: parse the address and start the service. Here, the address actually only needs to pass a string, but in order to achieve the effect of being able to pass or not pass, a variadic parameter is used. The resolveAddress method handles the results of different situations of addr.
Starting the service uses the ListenAndServe method in the net/http package of the standard library. This method accepts a listening address and a variable of the Handler interface. The definition of the Handler interface is very simple, with only one ServeHTTP method.

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

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
Enter fullscreen mode Exit fullscreen mode

Because the Engine implements ServeHTTP, the Engine itself will be passed to the ListenAndServe method here. When there is a new connection to the monitored port, ListenAndServe will be responsible for accepting and establishing the connection, and when there is data on the connection, it will call the ServeHTTP method of the handler for processing.

Process Messages

The ServeHTTP of the Engine is the callback function for processing messages. Let's take a look at its content.

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) 
}
Enter fullscreen mode Exit fullscreen mode

The callback function has two parameters. The first one is w which is used to receive the request reply. Write the reply data to w. The other is req which holds the data of this request. All data required for subsequent processing can be read from req.
The ServeHTTP method does four things. First, get a Context from the pool pool, then bind the Context to the parameters of the callback function, then call the handleHTTPRequest method with the Context as a parameter to process this network request, and finally put the Context back into the pool.
Let's first only look at the core part of the handleHTTPRequest method.

func (engine *Engine) handleHTTPRequest(c *Context) {
    //... Omit some code
    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, c.skippedNodes, unescape)
        //... Omit some code
        if value.handlers!= nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        //... Omit some code
    }
    //... Omit some code
}
Enter fullscreen mode Exit fullscreen mode

The handleHTTPRequest method mainly does two things. First, get the previously registered method from the radix tree according to the address of the request. Here, the handlers will be assigned to the Context for this processing, and then call the Next function of the Context to execute the methods in the handlers. Finally, write the return data of this request in the responseWriter type object of the Context.

Context

When processing an HTTP request, all context-related data are in the Context variable. The author also wrote in the comment of the Context struct that "Context is the most important part of gin", which shows its importance.
When talking about the ServeHTTP method of the Engine above, it can be seen that the Context is not directly created, but obtained through the Get method of the pool variable of the Engine. After being taken out, its state is reset before use, and it is put back into the pool after use.
The pool variable of the Engine is of type sync.Pool. For now, just know that it is an object pool provided by the Go official that supports concurrent use. You can get an object from the pool through its Get method, and you can also put an object into the pool using the Put method. When the pool is empty and the Get method is used, it will create an object through its own New method and return it.
This New method is defined in the New method of the Engine. Let's take another look at the New method of the Engine.

func New() *Engine {
    //... Omit other code
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}
Enter fullscreen mode Exit fullscreen mode

It can be seen from the code that the creation method of the Context is the allocateContext method of the Engine. There is no mystery in the allocateContext method. It just does two-step pre-allocation of slice lengths, then creates the object and returns it.

func (engine *Engine) allocateContext() *Context {
    v := make(Params, 0, engine.maxParams)
    skippedNodes := make([]skippedNode, 0, engine.maxSections)
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
Enter fullscreen mode Exit fullscreen mode

The Next method of the Context mentioned above will execute all the methods in the handlers. Let's take a look at its implementation.

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

Although handlers is a slice, the Next method is not simply implemented as a traversal of handlers, but introduces a processing progress record index, which is initialized to 0, incremented at the beginning of the method, and incremented again after a method execution is completed.

The design of Next has a great relationship with its usage, mainly to cooperate with some middleware functions. For example, when a panic is triggered during the execution of a certain handler, the error can be caught using recover in the middleware, and then Next can be called again to continue executing the subsequent handlers without affecting the entire handlers array due to the problem of one handler.

Handle Panic

In Gin, if the processing function of a certain request triggers a panic, the entire framework will not directly crash. Instead, an error message will be thrown, and the service will continue to be provided. It is somewhat similar to how Lua frameworks usually use xpcall to execute message processing functions. This operation is the "Crash-free" feature point mentioned in the official documentation.
As mentioned above, when using gin.Default to create an Engine, the Use method of the Engine will be executed to import two functions. One of them is the return value of the Recovery function, which is a wrapper of other functions. The final called function is CustomRecoveryWithWriter. Let's take a look at the implementation of this function.

func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
    //... Omit other code
    return func(c *Context) {
        defer func() {
            if err := recover(); err!= nil {
                //... Error handling code
            }
        }()
        c.Next() // Execute the next handler
    }
}
Enter fullscreen mode Exit fullscreen mode

We don't focus on the details of error handling here, but only look at what it does. This function returns an anonymous function. In this anonymous function, another anonymous function is registered using defer. In this inner anonymous function, recover is used to catch the panic, and then error handling is performed. After the handling is completed, the Next method of the Context is called, so that the handlers of the Context that were originally being executed in sequence can continue to be executed.

Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis

Finally, let me introduce the best platform for deploying Gin services: Leapcell.

Image description

1. Multi-Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Top comments (0)