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
- gin.Engine
- gin.RouterGroup
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:
Engine
:Focus on how HTTP requests are received and then abstracted into internal data structures;RouterGroup
:Focus on how to handle HTTP requests, with the core being the construction of theRouter
and the finding of theHandler
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:
[root@liqiang.io]# cat main.go
func main() {
gin.Default().GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, map[string]string{})
})
gin.Default().Run(":8080")
}
It covers the basics of Gin now:
- First the function
gin.Default().GET
, which is a typical registration route method; - Then there is the
gin.Default().Run
function, which runs an HTTP Server and will accept requests;
The first thing we might wonder about is what this gin.Default()
is. The answer can be found quite obviously in the code
[root@liqiang.io]# cat gin.go
func Default() *Engine {
... ...
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
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
[root@liqiang.io]# cat routergroup.go
func (group *RouterGroup) GET(relativePath string, handlers . .HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
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
[root@liqiang.io]# cat routergroup.go
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()
}
You can see here that RouterGroup
does only two things:
- Reconstruct
handlers
:handlers
can be interpreted as HTTP business processing functions, more on that later when we look at Middleware; - Add a route to
group.engine
: thisengine
is actually an instance ofEngine
.- The implementation here looks a bit weird, as
Engine
already containsRouterGroup
, andRouterGroup
contains anEngine
, so can we just useEngine
? - Theoretically, yes, but in practice
RouterGroup
performs theGroup
function, carrying a set of routing information and configuration; Engine
is the concept of a globalRouterGroup
, which allows multipleRouterGroup
s to be hosted, and which will all eventually map to the globalRouterGroup
;
- The implementation here looks a bit weird, as
[root@liqiang.io]# cat gin.go
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)
... ...
}
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:
[root@liqiang.io]# cat gin.go
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool. (*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
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:
[root@liqiang.io]# cat gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
// Find the root of the tree for the given HTTP method
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)
if value.params ! = nil {
c.Params = *value.params
}
if value.handlers ! = nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
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:
- The URL exists, but the requested method is not supported
- The requested URL does not exist
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:
[root@liqiang.io]# cat gin.go
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
If you want to customized the process function, it’s quite easy:
[root@liqiang.io]# cat main.go
server := gin.New()
server.NoMethod(func(c *gin.Context) {
log4go.Error("Method not found")
})
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:
[root@liqiang.io]# cat gin.go
c.handlers = engine.allNoRoute
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:
[root@liqiang.io]# cat main.go
server := gin.New()
server.NoRoute(func(c *gin.Context) {
log4go.Error("Route not found")
})
Field descriptions
In Engine, you may find two fields that seem to be similar:
[root@liqiang.io]# cat engine.go
type Engine struct {
RouterGroup
... ...
allNoRoute HandlersChain
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:
allNoRoute
: this method calls the Middlewares you added so that your Middleware can do some extra processing in the event of a 404 error, such as logging and monitoring data logging and the like;noRoute
: this is a simple Gin Handler that doesn’t call Middleware and is only used directly instaticHandler
;
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.