About WEB Frameworks

Gin is go’s lightweight Web framework, and being lightweight means providing only the basic functionality that a Web framework should have. I think the best way to look at source code is to have goals. Looking at the Web framework gin, my goals are as follows:

  1. How does gin implement the basic functionality of a Web framework
  2. What is there to learn from the implementation of the code?

The general process of processing a request

How to find the entrance

To understand the general flow of a request processing, just find the entry to the Web framework. Let’s start with the simplest demo in the GIN documentation. ListenAndServe, which means that the Engine structure implements the server HTTP interface. The entry point is the ServeHTTP interface implemented by the Engine.

// I am the simplest demo funcmain() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
		c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin"Func (engine * engine) Run(addr... func (engine * engine) Run(addr... string) (err error) { deferfunc() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}
Copy the code

ServeHTTP

The general process is as simple as a comment. It is important to note that the Context object is retrieved from the object pool, rather than generated every time. As you can see, the real core processing flow is in the handleHTTPRequest method.

func (engine *Engine) ServeHTTP(w http.ResponseWriter, Req * http.request) {// Get a Context object from the pool c := engine.pool.get ().(*Context) C.iterme.reset (w) c.equest = req c.reset() // Handles web requests engine.handleHttprequest (c) // throws the Context object back to the object pool engine.pool.Put(c) }Copy the code

handleHTTPRequest

The following code omits a lot of code that has nothing to do with the core logic, which is simple: find the handlers with the request method and the request URI and call them. Why the handlers and not the handlers that we wrote? Because it includes the handler for the middle layer.

func (engine *Engine) handleHTTPRequest(context *Context) { httpMethod := context.Request.Method var path string var Unescape bool // omitted...... // tree is an array containing a tree of urIs and handlers for each request. T := engine.trees t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {
		
		ifHandlers, params, TSR := handler. GetValue (path, handlers, TSR := handler. Context. Params, unescape) // Call the handlersifhandlers ! = nil { context.handlers = handlers context.Params = params context.Next() context.writermem.WriteHeaderNow()return} // omit......break}} // omit...... }Copy the code

A place to appreciate and learn

The routing process

Key requirements

Aside from the GIN framework, what are the key requirements for routing processing? Personally, there are two points

  • Lookup of efficient URI corresponding handler functions
  • Flexible route combination

The processing of gin

The core idea

  • Each route has a separate array of handlers
  • Middleware and processing functions are aligned
  • Use trees to provide efficient lookups of urIs corresponding to an array of processing functions

Interesting places

RouterGroup Indicates routing processing

Flexible route composition is achieved by applying a separate array of handlers to each URI. RouterGroup structure is abstracted to deal with the operation of route combination. Its main functions are:

  • Associate routes with associated processing functions
  • Provides routing group functionality, which is implemented due to the way prefixes are associated
  • Provides free combination of middleware functions: 1. Total middleware 2. Middleware of routing group 3. Middleware for processing functions

Both routing groups and processing functions can add middleware, which is much more flexible than DJango’s generic middleware.

Middleware processing

The middleware needs to handle it when the request is made and may need to handle it when it is returned. The picture below is django.

// Call the array func (c *Context)Next() {
	c.index++
	s := int8(len(c.handlers))
	for; c.index < s; C. Index ++ {c.handlers[C. Index](c)}} // Middleware example func Logger() gin.return func(c *gin.Context) {
		t := time.Now()

		// before request
		c.Set("example"."12345"Latency := time.since (t) log.print ()"latency: ", latency)

		status := c.Writer.Status()
		log.Println("status: ", status)
	}
}

func main() {
	r := gin.New()
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example").(string)

		// it would print: "12345"
		log.Println("example"}) // Listen and serve on 0.0.0.0:8080 r.run (": 8081")}Copy the code

Processing of requested and returned content

demand

  • Gets the parameters in the path
  • Get request parameters
  • Get request content
  • Returns the processed result

Gin framework implementation ideas

In addition to providing consistent processing, if you are not happy with the official implementation, you can replace it, or even add a layer of caching (which is not necessary, as normal use only handles once).

  • If the official HTTP library can provide it, then only one layer is wrapped over the official HTTP library, providing a consistent interface to the experience.
  • If the official HTTP library cannot provide it, implement it yourself

Key structure

typeContext struct {writermem responseWriter Request *http.Request Handlers HandlersChain (int8, int8, int8, int8, int8 *Engine Keys map[string]interface{} error errorMsgs Accepted []string}Copy the code

Points worth learning

Finding values in small numbers using arrays is faster than finding values in dictionaries

In the Context structure comment above, you can see that Params is actually an array. It’s essentially a key, so why not use a dictionary instead of an array? In real scenarios, the number of parameters to get path parameters is not very large, and the performance of dictionaries is not as good as that of arrays. Because the dictionary needs to find the corresponding value, the general process is as follows: Hash the key – > use an algorithm to find the corresponding offset position (there are several algorithms, you can check them if you are interested) – > value. A set of procedures, the array in a small number of cases, has been traversed.

router.GET("user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is222 " + action
		c.String(http.StatusOK, message)
	})
   
func (ps Params) Get(name string) (string, bool) {
	for _, entry := range ps {
		if entry.Key == name {
			return entry.Value, true}}return "".false
}
Copy the code

The same and different scenarios are handled through interfaces

Get request content

Scenarios facing this requirement for retrieving the requested content. For a static language like GO, if the request content is to be processed, it needs to be deserialized into a structure. However, the request content can take many forms, such as JSON, XML, ProtoBuf, and so on. Therefore, the following non-functional requirements can be summarized here.

  • Different content requires different deserialization mechanisms
  • Allow users to implement their own deserialization mechanism

The common ground is to deal with the content, the difference is to deal with the content of the way is not the same, it is easy to think of the concept of polymorphism, different seek common. The core of polymorphism is interface, at this time need to abstract an interface.

type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}
Copy the code

Returns the processed content

The requests vary, and the returns are the same. For example: return JSON, return XML, return HTML, return 302, etc. The following non-functional requirements can be summarized here.

  • Different types of returned content require different serialization mechanisms
  • Allows users to implement their own serialization mechanism

Same as above, so there’s also an interface abstraction here.

type Render interface {
	Render(http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}
Copy the code

Once you’ve defined the interface, how do you use it

Think about how to use these interfaces elegantly

For retrieving the requested content, within the model binding, there are the following scenarios

  • Binding failure is handled by users themselves or by the framework
  • Whether the user needs to choose a different binder for the content of the request

The answer given in the GIN framework for these scenarios is to provide different methods to meet the above requirements. Again, the key here is what the usage scenario is.

Func (c *Context) Bind(obj interface{}) error {b := binding.default (c.equest.method, c.ContentType())returnC. mustbindwith (obj, b)} Func (c *Context) MustBindWith(obj interface{}, b bind.binding) (err error) {iferr = c.ShouldBindWith(obj, b); err ! = nil { c.AbortWithError(400, err).SetType(ErrorTypeBind) }return} // The user can choose the binder and handle the error by himself. Self-selecting binders also means that users can implement their own binders. / / such as: The default JSON processing is the official JSON processing package, which is slow. Func (c *Context) ShouldBindWith(obj interface{}, b Binding.Binding) error {return b.Bind(c.Request, obj)
}
Copy the code

Handling of inconsistencies in the construction of implemented constructs

The class construction parameters of the implementation are inconsistent when the processed content is returned. For example, text processing and JSON processing. The weapon presented in this scenario is an extra layer of encapsulation, which is used to construct the corresponding processing object.

// Handle StringtypeString struct {Format String Data []interface{}} func (c *Context) String(code int, Format String, values ... interface{}) { c.Render(code, render.String{Format: format, Data: Json struct {Data interface{}} func (c *Context) json (code int, obj interface{}) { c.Render(code, render.JSON{Data: Func (c *Context) Render(code int, r Render.Render) {c.status (code)if! bodyAllowedForStatus(code) { r.WriteContentType(c.Writer) c.Writer.WriteHeaderNow()return
	}

	iferr := r.Render(c.Writer); err ! = nil { panic(err) } }Copy the code

conclusion

This process of looking at the code is done step by step after you have a goal, following the example of the official documentation. Then take a look at how the framework handles some of the scenarios common to Web frameworks. The framework has very little code and is elegantly written and well worth a look.