SoFunction
Updated on 2025-03-05

Summary of four ways to implement middleware framework in Golang

Write in front

middleware is a commonly used form in general frameworks, such as web frameworks and rpc frameworks. Through middleware, it does some public things at traffic entrances and exits, including authentication, logging, burying points, statistics, current limiting, parameter processing, exception processing, etc.

It is often used in work, and it will also be encountered when reading web frameworks (gin, beego). Today I will summarize what implementation methods are available in middleware.

Solution 1: Array recursive call

package middleware

import "context"

// Processing functionstype Handler func(ctx ,
	msg string) error

// Plug-in typetype MiddleWareFunc func(ctx ,
	msg string, next Handler) error

type MiddlewareManager struct {
	handler     Handler
	middlewares []MiddleWareFunc
}

func NewMiddlewareManager(handler Handler) *MiddlewareManager {
	return &MiddlewareManager{
		handler: handler,
	}
}

func (m *MiddlewareManager) Register(middlewares ...MiddleWareFunc) {
	 = append(, middlewares...)
}

func (m *MiddlewareManager) Exec(ctx , msg string) error {
	handlerFunc := func(ctx , msg string, next Handler) error {
		return (ctx, msg)
	}
	 = append(, handlerFunc)

	callChain := ()
	return callChain(ctx, msg)
}

func (m *MiddlewareManager) mkCallChain(
	middlewares []MiddleWareFunc) Handler {
	if len(middlewares) <= 0 {
		return nil
	}

	return func(ctx , msg string) error {
		return middlewares[0](ctx, msg, (middlewares[1:]))
	}
}

existMiddlewareManagerDefine business processing functions in structurehandlerand plugin arraysmiddlewares, executing the functionExecInside, the business processing function ishandlerEncapsulate it into a middleware and put it inmiddlewaresAfterwards, then recursively call the internal functionmkCallChain. This internal functionmkCallChainReturns a function that wraps all middleware layer by layer, and finallycallChain := ()What you get is a call chain.

This code is a bit tangible and needs to be carefully savored.

Test plan 1

	// Plan 1	("===Plan 1 begin")
	m1 := (HandlerMsg)
	(, , )
	if err := ((), "hello chain"); err != nil {
		panic(err)
	}
	("===Plan 1 end")

result

===Scheme 1 begin
TimeCost before
FinlterMW begin
LoggerMW before
HandlerMsg: hello chain
LoggerMW end
FinlterMW end
TimeCostMW:cost 1000428754
===Scheme one end

Solution 2: Sequential implementation

package middlewarecontext

type MiddleWareFunc func(ctx *MyContext) error

type MyContext struct {
	middlewares []MiddleWareFunc
	idx         int
	maxIdx      int
}

func NewMyContext() *MyContext {
	return &MyContext{
		middlewares: make([]MiddleWareFunc, 0),
	}
}

// Execute the next middlewarefunc (m *MyContext) Next() error {
	if  < -1 {
		 += 1
		return [](m)
	}

	return nil
}

// Terminate middlewarefunc (m *MyContext) Abort() {
	 = 
}

func (m *MyContext) Register(middlewares ...MiddleWareFunc) {
	 = append(, middlewares...)
	 = len()
}

func (m *MyContext) Exec() error {
	// Start execution from the first middleware	return [0](m)
}

This is the core code

type MyContext struct {
	middlewares []MiddleWareFunc
	idx         int
	maxIdx      int
}

Define a context by yourself and place all middleware as an array in the context, executeExec()When the first middleware is executed, the context is passed in. Other middlewaer callsNext()Function to trigger the next middleware.

This method looks simple and easy to understand. This is how the middleware of the gin framework is implemented. This method is the author's summary and abstraction of the middleware of the gin framework.

Test plan 2

	("===Plan 2 begin")
	m2 := ()
	(
		,
		,
		)
	if err := (); err != nil {
		panic(err)
	}
	("===Plan 2 end")

result

===Scheme 2 begin
TimeCost before
FinlterMW begin
LoggerMW before
LoggerMW end
FinlterMW end
TimeCostMW:cost 1000588399
===Scheme 2 end

Method 3: Chain call

package middlewarechain

import "context"

type Handler func(ctx ) error

type MiddleWareFunc func(ctx , next Handler) Handler

This code logic is very simple. It is to pass the previous middlewear as next parameter to the current middleware to form a chain call.

Do you find it strange to see this definition? Why are there such a little code?

Yes, it has so little code. There is a saying that goes, "There is no such thing as peace of mind, but someone is carrying the burden on you, and life is never easy." If there are fewer codes in the definition, it will definitely be complicated when calling.

Let's take a look at the test cases below

	("===Plan 3 begin")
	ctx := ()
	m3 := (ctx, func(ctx ) error {
		PrintMsg("test")
		return nil
	})
	m4 := (ctx, m3)
	m5 := (ctx, m4)
	if err := m5(ctx); err != nil {
		(err)
	}
	("===Plan 3 end")

result

===Scheme 3 begin
LoggerMW before
FinlterMW begin
TimeCost before
PrintMsg:test
TimeCostMW:cost 6130
FinlterMW end
LoggerMW end
===Scheme 3 end

It can be seen that when defining middlewear, the previous middleeeware must be passed into the current middleeeware definition. Compared with other solutions, it actually removes the middleware registration, and there is no place to maintain all middleware.

Solution 4: For loop implementation

package middlewarefor

import "context"

type Handler func(ctx ) error

type Middleware func(next Handler) Handler

type MiddlewareManager struct {
	middlewares []Middleware
}

func NewMiddlewareManager(middlewares ...Middleware) *MiddlewareManager {
	return &MiddlewareManager{
		middlewares: middlewares,
	}
}

func (m *MiddlewareManager) Register(middlewares ...Middleware) {
	 = append(, middlewares...)
}

func (m *MiddlewareManager) Exec(ctx ) error {
	handler := defaultHandler
	for i := range  {
		handler = [len()-i-1](handler)
	}

	return handler(ctx)
}

func defaultHandler(ctx ) error {
	return nil
}

It is very similar to the solution, both of which define oneMiddlewareManagerStructure, internal maintenancemiddlewaresArray, invokedExecWhen the loop is executedmiddlewares

Test plan 4

	("===Plan 4 begin")
	ctx = ()
	middleware4 := (
		,
		,
		,
	)

	(ctx)
	("===Plan 4 end")

result

===Scheme 4 begin
2023/01/15 15:27:09 [RecoveryMW] befor
2023/01/15 15:27:09 [LoggerMW] befor
2023/01/15 15:27:09 [TimeCostMW] cost:0.000000s
2023/01/15 15:27:09 [LoggerMW] end
2023/01/15 15:27:09 [RecoveryMW] end
===Scheme 4 end

Summarize

The above four solutions can all implement middleware. No evaluation is given, and you can use the same method you like.

The code implementation on this article and github is mainly used for learning and summary. If you want to use some way to your project, just copy it directly. It is not recommended to reference this code repository.

Github code repository:/ZBIGBEAR/middleware

This is the end of this article about the four ways to implement the middleware framework in Golang. For more related Go middleware framework content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!