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 indexindex
Is set toabortIndex
, which is more than Gin can supportHandlerFunc
Large number of- Call if a request is preprocessed and postprocessed
Next()
Method, the code before calling this method is pre-processing, and the code after that is post-processing.