3

Go framework Gin: HTTP Handling

 1 year ago
source link: https://en.liqiang.io/post/gin-how-router-and-middleware-works-e40ede92?lang=US_EN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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
64717ab95d6a.png

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 the Router and the finding of the Handler

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. [[email protected]]# 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:

  • 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



  1. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# 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:

  • 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: this engine is actually an instance of Engine.
    • The implementation here looks a bit weird, as Engine already contains RouterGroup, and RouterGroup contains an Engine, so can we just use Engine?
    • Theoretically, yes, but in practice RouterGroup performs the Group function, carrying a set of routing information and configuration;
    • Engine is the concept of a global RouterGroup, which allows multiple RouterGroups to be hosted, and which will all eventually map to the global RouterGroup;


  1. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# 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:

  • 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:



  1. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# 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:

  • 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 in staticHandler;

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK