This is the second day of my participation in the November Gwen Challenge. Check out the details: the last Gwen Challenge 2021

primers

First, let’s look beyond the implementation details of Gin and see what we do with middleware:

  • Permission to check
  • logging
  • Interface access verification

.

For those of you who have learned Java, when you first encounter Gin’s middleware, you will feel something like: “Isn’t that a filter/interceptor?”

Yes, from a pragmatic point of view Gin’s so-called middleware is essentially a generic process control for developers. We can centralize some common process control code in middleware, such as permission validation, logging and waiting.

If we had to implement an intermediate mechanism like Gin ourselves, what would you do?

First let’s think about what a middleware system should do:

  • Support for freely composing middleware functions
  • The system calls the middleware functions in the order in which we combine the middleware
  • Intermediate functions can intercept requests, that is, requests are not executed by business functions or the next intermediate (this scenario is often used for permission verification mechanisms)

To sum up, we can know that the main purpose of our intermediate system management is to manage the call relationship of middleware functions. You can manage intermediate functions through arrays or linked lists, but as functions are first class citizens of Golang, we naturally implemented a simple middleware system using closures.

Since we are going to use the closure mechanism to implement middleware functionality, all our intermediate functions need to do is wrap the business function and return a wrapped business function, so we can implement freely composed intermediate functions.

func Middleware(handler func(a)) func(a) {
	return func(a) {
		// do something before handle
		handler()
		// do something after handle}}Copy the code

We define a business function as a function with no incoming or outgoing parameters, and for testing we implement a function that will sleep randomly for 0 to 2 seconds to simulate business processing.

func FakeBusiness(a) {
	n := rand.Intn(3)
	time.Sleep( time.Duration(n) * time.Second )
	fmt.Printf("Job done time cost %ds\n", n)
}
Copy the code

Next we define three intermediate functions and assemble them with business functions

//Log Records logs
func Log(next func(a)) func(a) {
	return func(a) {
		fmt.Printf("[Log] Before Log \n")
		next()
		fmt.Printf("[Log] After Log \n")}}//RandomAbort rejects the request at random. If next is not executed, subsequent intermediate functions and business functions are not executed
func RandomAbort(next func(a)) func(a) {
	return func(a) {
		n := rand.Intn(10)
		if n % 2= =0 {
			fmt.Printf("[RandomAbort] abort %d ! \n", n)
			return
		}
		// call only when odd
		fmt.Printf("[RandomAbort] n=%d Not abort! execute next\n", n)
		next()
		fmt.Printf("[RandomAbort] execute next finished \n")}}//Timecost Records the time spent by a business function
func Timecost(next func(a)) func(a) {
	return func(a) {
		start := time.Now()
		fmt.Printf("[Timecost] Before handle %d\n", start.Unix())
		next()
		fmt.Printf("[Timecost] After handle %d timecost %dms\n",
			time.Now().Unix(), time.Since(start).Milliseconds())
	}
}
Copy the code

Combine (Since the intermediate function returns the wrapped business function, we are free to combine the middleware.)

func main(a) {
	rand.Seed(time.Now().Unix())
	handlerChain := Timecost(Log(RandomAbort(FakeBusiness)))
	handlerChain()
}

Copy the code

The combined middleware and business functions look like this:

You can see that the combined middleware and business functions are essentially a callstack.

Gin middleware implementation

Gin takes a less cognitively burdsome approach to implementation than closures, with the underlying use of an array of functions to hold the middleware, defined as follows:

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array. 
type HandlersChain []HandlerFunc
Copy the code

Each time a request reaches the server, Gin allocates a Context to the request, which holds the handler chain corresponding to the request, and an index, IDNEx, which records which HandlerFunc is currently being processed, with an initial value of -1 for index

type Context struct{... handlers HandlersChain indexint8.Copy the code

When the Context is constructed, Gin calls the Context’s Next method to begin processing the request.

func (c *Context) Next(a) {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}
Copy the code

If you want to implement the nested call effect described above, We simply call Conext’s Next() method in HandlerFunc, at which point the context increses the index to execute the Next HandlerFunc. When the function call stack returns to the current method, since the index is larger than the size of the HandlerChain, There will be no duplication of execution.

If you have noticed that Gin imposes a limit on the number of handlerFuncs. Gin can only support 127 handlerFunCs. However, Gin actually compares the size of the HandlerChain to abortIndex when calling the Use method to add an intermediate. If the value is greater than this, an error is reported.

// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 / / 63

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}
Copy the code

So if we want to terminate the request early we just set index to abortIndex.

func (c *Context) Abort(a) {
	c.index = abortIndex
}
Copy the code

After modifying the above example, we have the following code:

	r := gin.Default()
	// time cost
	r.Use(func(context *gin.Context) {
		start := time.Now()
		context.Next()
		fmt.Printf("time cost %d ms\n", time.Since(start).Milliseconds())
	})
	// random abort
	r.Use(func(context *gin.Context) {
		if rand.Intn(10) % 2= =0 {
			context.JSON(200.map[string]interface{} {"Message": fmt.Sprintf("Request abort"),
				"Code": - 1,
			})
			context.Abort()
		}
	})
	// log
	r.Use(func(context *gin.Context) {
		fmt.Printf("before handle\n")
		context.Next()
		fmt.Printf("after handle\n")})// business
	r.GET("/test".func(context *gin.Context) {
		context.JSON(200.map[string]interface{} {"Message": "HelloWorld"."Code": 0,
		})
	})
	r.Run(": 8080")
Copy the code

conclusion

Gin’s middleware system maintains an array of functions, HandlerChain, and an index, to store middleware functions and interface handlers. And:

  • Middleware functions are run in the order they are added
  • Abort()The principle is to indexindexIs set toabortIndex, which is more than Gin can supportHandlerFuncLarge number of
  • Call if a request is preprocessed and postprocessedNext()Method, the code before calling this method is pre-processing, and the code after that is post-processing.