4、中间件模式:装饰器模式、责任链模式、Go中的函数式中间件

好,咱们今天聊点实在的。中间件模式,说白了就是给核心逻辑「穿衣服」的过程。你想想看,一个 HTTP 请求进来,总得先做点日志、鉴权、限流之类的事情吧?这些事如果全塞进业务代码里,那代码就成了一锅粥。

我最早接触中间件,是在写 Java 的 Filter 时。那时候觉得这东西挺神奇,后来转到 Go 才发现,Go 的函数式特性让中间件变得特别优雅。今天咱们就拆开揉碎了讲清楚。

4.1 装饰器模式:给函数「叠 buff」

装饰器模式,说白了就是给一个函数套上一层又一层的功能。就像游戏里给角色加 buff,每个 buff 都不改变角色本身,但增强了能力。

在 Go 里,装饰器通常长这样:

type Handler func(req *Request) *Response

func WithLogging(next Handler) Handler {
    return func(req *Request) *Response {
        log.Printf("request: %v", req)
        resp := next(req)
        log.Printf("response: %v", resp)
        return resp
    }
}

func WithAuth(next Handler) Handler {
    return func(req *Request) *Response {
        if !isValidToken(req.Token) {
            return &Response{Status: 401}
        }
        return next(req)
    }
}

你看,WithLoggingWithAuth 都是装饰器。它们接收一个 Handler,返回一个新的 Handler。核心业务逻辑完全不用改。

核心要点:装饰器模式的关键在于「组合」。你可以把多个装饰器叠在一起,像搭积木一样。

我在项目中遇到过一种情况:团队里有人把日志、鉴权、限流全写在一个函数里。结果后来要调整鉴权顺序,改得头皮发麻。用装饰器模式,顺序调整就是换一行代码的事。

4.2 责任链模式:请求的「流水线」

责任链模式,你可以把它想象成一条流水线。每个节点只处理自己关心的事,处理完就传给下一个。如果某个节点觉得「这事我搞不定」,它可以选择不处理,直接往后传。

Go 里实现责任链,我习惯用切片:

type Middleware func(next Handler) Handler

type Pipeline struct {
    middlewares []Middleware
}

func (p *Pipeline) Use(m Middleware) {
    p.middlewares = append(p.middlewares, m)
}

func (p *Pipeline) Then(handler Handler) Handler {
    for i := len(p.middlewares) - 1; i >= 0; i-- {
        handler = p.middlewares[i](handler)
    }
    return handler
}

嗯,这里要注意:Then 方法是从后往前遍历的。为什么?因为中间件的执行顺序是「先注册的先执行」。你想想看,如果从前往后遍历,那最后一个注册的中间件反而最先执行,这就不对了。

小技巧:如果你想让代码更直观,也可以从前往后遍历,但那样需要把中间件列表反转一下。我个人习惯用从后往前的方式,省一次反转。

4.3 Go 中的函数式中间件

Go 虽然不是纯函数式语言,但它的函数是一等公民。这意味着我们可以把中间件当成值来传递、组合。

最常见的模式就是 func(http.Handler) http.Handler。标准库的 net/http 就是这么设计的:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

然后你可以这样用:

mux := http.NewServeMux()
mux.Handle("/api", authMiddleware(loggingMiddleware(finalHandler)))

是不是很简洁?但说实话,嵌套多了可读性会下降。所以我一般会封装一个 Chain 函数:

func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// 使用
mux.Handle("/api", Chain(finalHandler, loggingMiddleware, authMiddleware, rateLimitMiddleware))

避坑指南:我曾经在项目里遇到过一个 bug,中间件里修改了请求的 Body,但后面的中间件读不到。后来发现是因为 Body 是 io.ReadCloser,读完就没了。解决方案是在中间件里用 io.NopCloser 重新包装一下。

4.4 三种模式的对比

模式 核心思想 Go 实现方式 适用场景
装饰器模式 给函数叠加功能 返回新函数的函数 日志、监控、缓存
责任链模式 请求沿链传递 中间件切片 + 遍历 鉴权、限流、过滤
函数式中间件 函数作为一等公民 Handler 类型 + Chain 组合 HTTP 中间件、gRPC 拦截器

你可能会问:「这三种模式到底选哪个?」其实它们不是互斥的。装饰器模式是思想,责任链模式是结构,函数式中间件是 Go 的具体实现。说白了,你写 Go 中间件时,用的就是函数式的方式,背后同时体现了装饰器和责任链的思想。

4.5 实战:一个完整的中间件链

咱们来写一个真实点的例子。假设我们要做一个 API 网关,需要日志、鉴权、限流三个中间件:

type Context struct {
    Request  *http.Request
    Response http.ResponseWriter
    User     *User
    Errors   []error
}

type Middleware func(ctx *Context, next func())

func Logger(ctx *Context, next func()) {
    start := time.Now()
    next()
    log.Printf("%s %s took %v", ctx.Request.Method, ctx.Request.URL.Path, time.Since(start))
}

func Auth(ctx *Context, next func()) {
    token := ctx.Request.Header.Get("Authorization")
    user, err := validateToken(token)
    if err != nil {
        ctx.Response.WriteHeader(http.StatusUnauthorized)
        return
    }
    ctx.User = user
    next()
}

func RateLimit(ctx *Context, next func()) {
    if !allow(ctx.Request.RemoteAddr) {
        ctx.Response.WriteHeader(http.StatusTooManyRequests)
        return
    }
    next()
}

func Run(ctx *Context, middlewares []Middleware, handler func(*Context)) {
    var chain func(int)
    chain = func(index int) {
        if index >= len(middlewares) {
            handler(ctx)
            return
        }
        middlewares[index](ctx, func() { chain(index + 1) })
    }
    chain(0)
}

这个实现里,每个中间件都可以决定是否调用 next()。如果不调用,链就断了。比如鉴权失败时直接返回 401,后面的限流和业务逻辑就不会执行。

关键设计Context 结构体贯穿整个链,中间件可以在上面存数据(比如 User),后面的中间件和业务逻辑都能读到。这就是 Go 中常见的「上下文传递」模式。

好了,今天的内容就到这。装饰器模式、责任链模式、函数式中间件,这三者其实是一脉相承的。你写多了就会发现,中间件的本质就是「控制流的组合」。下一章咱们聊聊更进阶的话题——如何用泛型写出类型安全的中间件。