洋葱模型-中间件实现【golang】

洋葱模型

中间件,狭义概念为一个业务代码的过滤操作,对,就是日常web开发中你接触的那种中间件

在使用开源框架时,都已经做好了封装,采用的大多都是"洋葱模型"

洋葱,一层包裹着一层,层层递进,一刀切下来的横截面如图所示


洋葱.png

假设一个web请求过来,我们在内部定义了很多中间件,这个请求就像是穿越洋葱的包裹,到达洋葱最里面的心,在心部处理完后,再一层一层穿出去

这里有个特点:进入时穿入了多少层表皮,出去时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,符合我们所说的栈列表,先进后出的原则

洋葱模型_2.png

其实中间件也不是仅仅应用于web,同样对于cli工具也是适用的,比方说命令行输入值,进行一段逻辑操作后输出

编程实现

设计思路

定义两个方法,一个是业务逻辑方法,一个是中间件方法

设计时,业务逻辑应该独立运行的,假设业务逻辑为 func Pay

func Pay(请求体) {
    响应体
}

中间件的声明定义,也应当是完全独立的,要满足两个条件

  1. 中间件自身的初始化、自身逻辑
  2. 返回一个函数,用于业务逻辑与中间件关联

业务逻辑与中间件的关联,通过函数传参进去,假设中间件为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}

代码的编程思想,就是通过函数返回函数,来实现栈的先进后出

实现一个关于用户登陆逻辑,多个中间件中间件的洋葱模型

![顺序中间件.png](https://upload-images.jianshu.io/upload_images/24683335-da69db1d9464c7ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

中间件

  • 日志中间件
  • 参数校验中间件
  • 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为核心,本质上是将多个中间件,组合成一个中间件,主要做了两方面

  1. 控制先进后出
  2. 控制多个中间件,最后一个执行next,保证业务逻辑只执行一次

中间件顺序执行

业务场景中,有时需要顺序执行模型,就像代码顺序执行执行一样


顺序中间件.png

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容