Overview

As some of the services I maintain are written using the Gin framework, and some of the web services I used to work on were implemented using similar ideas to Gin, it was my duty to take a deeper look at the underlying implementation of Gin to identify problems and possible risks in time.

Gin is a popular HTTP framework that is used by a lot of people, but it is actually simple enough that there is not much to say about it. However, although there is not much to talk about, there are some descriptions and ideas that are worth mentioning, and this article is the first in a series of articles that will focus on how Gin handles HTTP requests. As Gin essentially extends the native HTTP Server, there is not much to cover in terms of the network model, so this article focuses on some of Gin’s key data structures and how they fit in with the native HTTP Server.

Key Structure

Figure 1:Relation between Engine and RouterGroup

Think of Gin’s routing module as really just these two key data structures, essentially Engine is also a RouterGroup, but with a different focus:

If anything, there is a core data structure: Context, which serves to concatenate the processing logic of HTTP requests, and this will be covered separately later.

Routing

To dive into the code, we can start with a simple example that explores the underlying implementation details, for example the following very simple Gin service:

  1. [root@liqiang.io]# cat main.go
  2. func main() {
  3. gin.Default().GET("/", func(c *gin.Context) {
  4. c.JSON(http.StatusOK, map[string]string{})
  5. })
  6. gin.Default().Run(":8080")
  7. }

It covers the basics of Gin now:

The first thing we might wonder about is what this gin.Default() is. The answer can be found quite obviously in the code

  1. [root@liqiang.io]# cat gin.go
  2. func Default() *Engine {
  3. ... ...
  4. engine := New()
  5. engine.Use(Logger(), Recovery())
  6. return engine
  7. }

This is an Engine data structure, and New() has some key data structures inside, which we’ll talk about as we go along later, starting with a look at how the functionality we’re concerned about is implemented.

Route registration

The registration of routes we already know is used by public methods like gin.Default().Get, but the underlying call actually looks like this

  1. [root@liqiang.io]# cat routergroup.go
  2. func (group *RouterGroup) GET(relativePath string, handlers . .HandlerFunc) IRoutes {
  3. return group.handle(http.MethodGet, relativePath, handlers)
  4. }

Here we see that the data structure has been moved from Engine to RouterGroup, which is the division of responsibilities I mentioned at the beginning, with the routing-related stuff going to RouterGroup. So how is all the routing information organised within the RouterGroup? The answer to this is to continue to look in routergroup.go in the root directory of gin

  1. [root@liqiang.io]# cat routergroup.go
  2. func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
  3. absolutePath := group.calculateAbsolutePath(relativePath)
  4. handlers = group.combineHandlers(handlers)
  5. group.engine.addRoute(httpMethod, absolutePath, handlers)
  6. return group.returnObj()
  7. }

You can see here that RouterGroup does only two things:

  1. [root@liqiang.io]# cat gin.go
  2. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  3. root := engine.trees.get(method)
  4. if root == nil {
  5. root = new(node)
  6. root.fullPath = "/"
  7. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  8. }
  9. root.addRoute(path, handlers)
  10. ... ...
  11. }

You can see some keywords here, such as engine.trees, and you can also see that the tree is organised in terms of the Request Method dimension, meaning that there is a separate tree for each HTTP method. Gin uses a modified version of the prefix tree, because the regular prefix tree is an exact match, but Gin can have wildcards and optional characters in the routing rules, making the tree more complex. Constructing](https://liqiang.io/post/gin-23-how-to-build-routing-tree-xho5Aa2u)), where I explain in detail how Gin constructs prefix trees for wildcards and optional characters.

You can assume that by adding these trees, the route information is added to Gin, and the registration of the route is now complete.

Route resolution

So once we know how many prefix trees are at work, when a request comes in, then we should be able to guess how Gin finds out what the corresponding processing function is for that request:

  1. [root@liqiang.io]# cat gin.go
  2. // ServeHTTP conforms to the http.Handler interface.
  3. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  4. c := engine.pool. (*Context)
  5. c.writermem.reset(w)
  6. c.Request = req
  7. c.reset()
  8. engine.handleHTTPRequest(c)
  9. engine.pool.Put(c)
  10. }

For those of you who are familiar with Go native HTTP, this is actually the prototype of the Go native HTTP handler, and Gin actually uses this data structure as the entry point to the native HTTP Library, so if you’re asking what Gin’s underlying network model is? The answer is that there is no special network model, it is the Go HTTP Server’s native network model.

The core of Gin’s HTTP processing portal is one: building the Context, which was introduced at the beginning and is one of Gin’s key data structures. And you can see here, it use a pool technology, and the detail about it I have put it in the later article: Go Framework Gin:Context

To cut to the chase, when Gin starts to process the request, the principle of finding the Handler starts with trees, and the code is actually relatively simple:

  1. [root@liqiang.io]# cat gin.go
  2. func (engine *Engine) handleHTTPRequest(c *Context) {
  3. // Find the root of the tree for the given HTTP method
  4. t := engine.trees
  5. for i, tl := 0, len(t); i < tl; i++ {
  6. if t[i].method ! = httpMethod {
  7. continue
  8. }
  9. root := t[i].root
  10. // Find route in tree
  11. value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
  12. if value.params ! = nil {
  13. c.Params = *value.params
  14. }
  15. if value.handlers ! = nil {
  16. c.handlers = value.handlers
  17. c.fullPath = value.fullPath
  18. c.Next()
  19. c.writermem.WriteHeaderNow()
  20. return
  21. }

As you can see, it starts with a Request Method as the dimension to find a particular tree, then matches the request within that tree, finds the request handler, and then calls the request handler, and the processing mechanism of this request handler is another exciting story, which we will cover in the following section on Middleware. so here’s how you can understand it. So how do you find it in a tree? This is actually also closely related to the construction of the prefix tree, so I’ll skip it here, and for those interested, jump to my article (Go Framework Gin: Construction of the Route Prefix Tree) for more information.

Exception handling

In addition to the normal routing handling, Gin pays extra attention to two cases:

Method Not Allowed

If the specified route handler is not found in the normal route, then Gin will try to see if another request method satisfies the requested URL, and if it does, then it will call a specific handler function that is user-defined:

  1. [root@liqiang.io]# cat gin.go
  2. if engine.HandleMethodNotAllowed {
  3. for _, tree := range engine.trees {
  4. if tree.method == httpMethod {
  5. continue
  6. }
  7. if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
  8. c.handlers = engine.allNoMethod
  9. serveError(c, http.StatusMethodNotAllowed, default405Body)
  10. return
  11. }
  12. }
  13. }

If you want to customized the process function, it’s quite easy:

  1. [root@liqiang.io]# cat main.go
  2. server := gin.New()
  3. server.NoMethod(func(c *gin.Context) {
  4. log4go.Error("Method not found")
  5. })

404 Not Found

If the previous normal route and URL can be found, but the specified request method cannot be found, then the processing logic for Not Found is extremely simple:

  1. [root@liqiang.io]# cat gin.go
  2. c.handlers = engine.allNoRoute
  3. serveError(c, http.StatusNotFound, default404Body)

This Not Found handler is also customisable and easy to customise, and it should be noted that, like the 405 handler, this one is at the global (not Group) level:

  1. [root@liqiang.io]# cat main.go
  2. server := gin.New()
  3. server.NoRoute(func(c *gin.Context) {
  4. log4go.Error("Route not found")
  5. })

Field descriptions

In Engine, you may find two fields that seem to be similar:

  1. [root@liqiang.io]# cat engine.go
  2. type Engine struct {
  3. RouterGroup
  4. ... ...
  5. allNoRoute HandlersChain
  6. noRoute HandlersChain

The difference between these two fields is that they come with or without the all prefix, so what is the difference between them? It’s actually quite simple:

Summary

In this article I’ve covered the key data structures of Gin and the process of receiving and processing HTTP requests, including the logic of Gin’s route registration and fetching. In fact, this is most of what Gin is about, and most of the fields in Engine are related to this, in addition to the fields not yet mentioned: Route tree construction and use, Template rendering, URL parameter fetching, which will be covered later in the series.

Ref