洋葱模型
中间件,狭义概念为一个业务代码的过滤操作,对,就是日常web开发中你接触的那种中间件
在使用开源框架时,都已经做好了封装,采用的大多都是"洋葱模型"
洋葱,一层包裹着一层,层层递进,一刀切下来的横截面如图所示

洋葱.png
假设一个web请求过来,我们在内部定义了很多中间件,这个请求就像是穿越洋葱的包裹,到达洋葱最里面的心,在心部处理完后,再一层一层穿出去
这里有个特点:进入时穿入了多少层表皮,出去时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,符合我们所说的栈列表,先进后出的原则

洋葱模型_2.png
其实中间件也不是仅仅应用于web,同样对于cli工具也是适用的,比方说命令行输入值,进行一段逻辑操作后输出
编程实现
设计思路
定义两个方法,一个是业务逻辑方法,一个是中间件方法
设计时,业务逻辑应该独立运行的,假设业务逻辑为 func Pay
func Pay(请求体) {
响应体
}
中间件的声明定义,也应当是完全独立的,要满足两个条件
- 中间件自身的初始化、自身逻辑
- 返回一个函数,用于业务逻辑与中间件关联
业务逻辑与中间件的关联,通过函数传参进去,假设中间件为func Middle
func Middle(中间件初始化参数) {
// 这里做一些中间件自身的初始化
// ……
// 这个Pay方法,就是我们业务逻辑要传入的方法
return func (Next func(请求体) 响应 ) {
// 这里为中间件已业务逻辑关联上
return func(请求体) { // 这里为中间件真正执行的逻辑
// 中间件前置操作
// Next(请求体) 这个就是业务逻辑的代码执行之处,就是Pay方法
// 中间件后置操作
return 响应
}
}
}
执行流程为
1. 中间件初始化,返回一个函数
func Middle(初始化参数) {
return func C( func(请求体) 响应 ) {
return func(请求体) {
响应
}
}
}
C := Middle()
2. 业务逻辑注入
// 业务逻辑方法
func Pay(请求体) {
响应
}
D := C(Pay)
// 这个 D 长这个样子
func D (请求体) {
响应
}
3. 业务代码代码执行
result := D(实际具体业务请求体)
实现一个关于支付逻辑,风控中间件,单层洋葱
package main
import (
"context"
"errors"
"fmt"
)
// 支付请求的结构体
type PayRequest struct {
UserID string // 用户ID
OrderID string // 订单ID
Amount int64 // 支付金额(分)
Sign string // 签名,用于验签
}
// 支付响应的结构体
type PayResponse struct {
Success bool
TradeNo string
}
// 定义中间件类型
type Handler func(ctx context.Context, req interface{}) (resp interface{}, err error)
type Middleware func(Handler) Handler
// 中间件方法定义,接收初始化参数,返回函数定义
func RiskControlMiddleware(blacklist map[string]bool) Middleware {
fmt.Println("Risk中间件初始化")
// 业务逻辑与中间件绑定 接收实际业务函数(这里是实际的支付逻辑),返回实际中间件执行逻辑
return func(next Handler) Handler {
fmt.Println("Risk中间件业务装载初始化")
// 中间件、业务逻辑实际执行逻辑
return func(ctx context.Context, req interface{}) (resp interface{}, err error) {
// ------中间件前置操作------
payReq, ok := req.(*PayRequest)
if !ok {
return nil, errors.New("请求格式错误")
}
fmt.Printf("[中间件前置操作] 用户:%s,IP:%s\n", payReq.UserID, payReq.OrderID)
if blacklist[payReq.UserID] {
return nil, errors.New("用户在黑名单中,拒绝支付")
}
if payReq.Sign != "valid_sign" {
return nil, errors.New("签名无效,拒绝支付")
}
// ------中间件前置操作------
// ------实际业务执行------
resp, err = next(ctx, req)
// ------实际业务执行------
// ------中间件后置操作------
fmt.Printf("[中间件后置操作] 用户:%s,IP:%s\n", payReq.UserID, payReq.OrderID)
// ------中间件后置操作------
return resp, err
}
}
}
// 业务逻辑
func payHandler(ctx context.Context, req interface{}) (resp interface{}, err error) {
payReq := req.(*PayRequest)
fmt.Printf("执行支付逻辑:用户[%s]支付订单[%s],金额[%d]分\n", payReq.UserID, payReq.OrderID, payReq.Amount)
// 模拟支付成功
return &PayResponse{Success: true, TradeNo: "TRADE123456"}, nil
}
func main() {
// 初始化黑名单(用户"user_999"是黑名单)
blacklist := map[string]bool{"user_999": true}
// 创建风控中间件,并包装支付处理器
riskMiddleware := RiskControlMiddleware(blacklist) // 1. 初始化中间件
wrappedHandler := riskMiddleware(payHandler) // 2. 业务逻辑与中间件绑定
// 模拟一个正常用户的支付请求
req := &PayRequest{
UserID: "user_123",
OrderID: "order_456",
Amount: 1000,
Sign: "valid_sign",
}
resp, err := wrappedHandler(context.Background(), req)
if err != nil {
fmt.Println("支付失败:", err)
return
}
fmt.Println("支付响应:", resp)
}
执行结果
Risk中间件初始化
Risk中间件业务装载初始化
[中间件前置操作] 用户:user_123,IP:order_456
执行支付逻辑:用户[user_123]支付订单[order_456],金额[1000]分
[中间件后置操作] 用户:user_123,IP:order_456
支付响应: &{true TRADE123456}
代码的编程思想,就是通过函数返回函数,来实现栈的先进后出
实现一个关于用户登陆逻辑,多个中间件中间件的洋葱模型


中间件
- 日志中间件
- 参数校验中间件
- IP黑名单中间件
业务逻辑
- 登陆业务逻辑
package middle
import (
"context"
"errors"
"fmt"
)
// 定义请求和响应结构
type LoginRequest struct {
Username string
Password string
IP string
}
type LoginResponse struct {
Success bool
Token string
}
// 定义处理器类型:核心业务逻辑的函数类型
type Handler func(ctx context.Context, req *LoginRequest) (*LoginResponse, error)
type Middleware func(Handler) Handler
// 日志中间件
func WithLogging() Middleware {
fmt.Println("[日志中间件] 初始化")
return func(next Handler) Handler {
fmt.Println("[日志中间件] 业务装载完成")
return func(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// ------中间件前置操作------
fmt.Printf("[日志中间件-前置操作] 用户:%s,IP:%s\n", req.Username, req.IP)
// ------中间件前置操作------
// ------实际业务执行------
resp, err := next(ctx, req)
// ------实际业务执行------
// ------中间件后置操作------
fmt.Printf("[日志中间件-后置操作] 用户:%s,IP:%s\n", req.Username, req.IP)
// ------中间件后置操作------
return resp, err
}
}
}
// 参数校验中间件
func WithParamCheck() Middleware {
fmt.Println("[参数校验中间件] 初始化")
return func(next Handler) Handler {
fmt.Println("[参数校验中间件] 业务装载完成")
return func(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// ------中间件前置操作------
if req.Username == "" || req.Password == "" {
return nil, errors.New("[参数校验] 账号或密码不能为空")
}
fmt.Printf("[参数校验中间件-前置操作] 用户:%s,IP:%s\n", req.Username, req.IP)
// ------中间件前置操作------
// ------实际业务执行------
resp, err := next(ctx, req)
// ------中间件后置操作------
fmt.Printf("[参数校验中间件-后置操作] 用户:%s,IP:%s\n", req.Username, req.IP)
// ------中间件后置操作------
return resp, err
}
}
}
// IP黑名单中间件
func WithIPBlacklist(blacklist map[string]bool) Middleware {
fmt.Println("[IP黑名单中间件] 初始化")
return func(next Handler) Handler {
fmt.Println("[IP黑名单中间件] 业务装载完成")
return func(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// ------中间件前置操作------
if blacklist[req.IP] {
return nil, errors.New("[IP黑名单] 危险IP,拒绝登录")
}
fmt.Printf("[IP中间件-前置操作] IP:%s\n", req.IP)
// ------中间件前置操作------
// ------实际业务执行------
resp, err := next(ctx, req)
// ------实际业务执行------
// ------中间件后置操作------
fmt.Printf("[IP中间件-后置操作] IP:%s\n", req.IP)
// ------中间件后置操作------
return resp, err
}
}
}
// 业务逻辑
func loginHandler(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// 模拟校验账号密码(实际中可能查数据库)
if req.Username == "admin" && req.Password == "123456" {
fmt.Println("[⭐️⭐️⭐️登录逻辑⭐️⭐️⭐️] 账号密码正确,登录成功")
return &LoginResponse{Success: true, Token: "token_123456"}, nil
}
return nil, errors.New("[⭐️⭐️⭐️登录逻辑⭐️⭐️⭐️] 账号或密码错误")
}
// 组合多个中间件的工具函数
func chain(handlers ...Middleware) Middleware {
return func(next Handler) Handler {
fmt.Printf("[中间件群组先进后出组装]\n")
// 洋葱一层一层的包裹过程
for i := len(handlers) - 1; i >= 0; i-- {
next = handlers[i](next)
}
return next
}
}
func Middle() {
// 初始化IP黑名单
ipBlacklist := map[string]bool{
"192.168.0.100": true, // 这个IP被拉黑
}
// 1. 按顺序定义中间件列表
fmt.Println("------[中间件] 初始化------")
middlewares := []Middleware{
WithLogging(), // 第1个执行(最外层)
WithParamCheck(), // 第2个执行
WithIPBlacklist(ipBlacklist), // 第3个执行(最内层,靠近核心逻辑)
}
fmt.Println("------[中间件] 初始化------")
// 2. 用chain函数组合中间件,包装核心登录逻辑
fmt.Println("------[中间件] 绑定------")
wrappedHandler := chain(middlewares...)(loginHandler)
fmt.Println("------[中间件] 绑定------")
// 3. 模拟一个合法的登录请求
req := &LoginRequest{
Username: "admin",
Password: "123456",
IP: "192.168.0.1", // 不在黑名单
}
fmt.Println("------[中间件] 执行------")
resp, err := wrappedHandler(context.Background(), req)
fmt.Println("------[中间件] 执行------")
if err != nil {
fmt.Println("登录失败:", err)
return
}
fmt.Println("登录成功,Token:", resp.Token)
}
输出
------[中间件] 初始化------
[日志中间件] 初始化
[参数校验中间件] 初始化
[IP黑名单中间件] 初始化
------[中间件] 初始化------
------[中间件] 绑定------
[中间件群组先进后出组装]
[IP黑名单中间件] 业务装载完成
[参数校验中间件] 业务装载完成
[日志中间件] 业务装载完成
------[中间件] 绑定------
------[中间件] 执行------
[日志中间件-前置操作] 用户:admin,IP:192.168.0.1
[参数校验中间件-前置操作] 用户:admin,IP:192.168.0.1
[IP中间件-前置操作] IP:192.168.0.1
[⭐️⭐️⭐️登录逻辑⭐️⭐️⭐️] 账号密码正确,登录成功
[IP中间件-后置操作] IP:192.168.0.1
[参数校验中间件-后置操作] 用户:admin,IP:192.168.0.1
[日志中间件-后置操作] 用户:admin,IP:192.168.0.1
这里的chain为核心,本质上是将多个中间件,组合成一个中间件,主要做了两方面
- 控制先进后出
- 控制多个中间件,最后一个执行next,保证业务逻辑只执行一次
中间件顺序执行
业务场景中,有时需要顺序执行模型,就像代码顺序执行执行一样

顺序中间件.png
这其实很简单,注释掉中间件的后置执行代码即可,例如 return next(ctx, req)放在中间件方法的最后一行