Skip to content
Go
中间件与拦截器

中间件与拦截器

go-zero 使用洋葱模型组织 HTTP 中间件和 gRPC 拦截器,支持声明式注册(.api DSL)和编程式注册两种方式。


HTTP 中间件

洋葱模型

请求 → 中间件B前置 → 中间件A前置 → Handler → 中间件A后置 → 中间件B后置 → 响应

💡 后添加的中间件先执行前置逻辑,最后执行后置逻辑(栈结构)。

中间件签名

type Middleware func(next http.HandlerFunc) http.HandlerFunc

自定义中间件的三种方式

方式1:API DSL 声明(推荐)

.api 文件中声明,goctl 自动生成骨架:

@server(
    middleware: Log,Auth
)
service user-api {
    @handler userInfo
    get /user/info/:id (UserInfoReq) returns (UserInfoResp)
}

运行 goctl api go 后,自动生成 internal/middleware/logmiddleware.go

// internal/middleware/logmiddleware.go
package middleware

import "net/http"

type LogMiddleware struct{}

func NewLogMiddleware() *LogMiddleware {
    return &LogMiddleware{}
}

func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO: 在此实现日志记录逻辑
        next(w, r)
    }
}

ServiceContext 中注册:

type ServiceContext struct {
    Config        config.Config
    LogMiddleware rest.Middleware
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:        c,
        LogMiddleware: middleware.NewLogMiddleware().Handle,
    }
}

路由自动注入(routes.go 自动生成):

func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
    server.AddRoutes(
        rest.WithMiddlewares(
            []rest.Middleware{serverCtx.LogMiddleware, serverCtx.AuthMiddleware},
            []rest.Route{
                {
                    Method:  http.MethodGet,
                    Path:    "/user/info/:id",
                    Handler: userInfoHandler(serverCtx),
                },
            }...,
        ),
    )
}

方式2:全局中间件(main.go)

func main() {
    server := rest.MustNewServer(c.RestConf)

    // 全局中间件:所有请求都经过
    server.Use(func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            defer func() {
                logx.Infof("request: %s %s, duration: %v", r.Method, r.URL.Path, time.Since(start))
            }()
            next(w, r)
        }
    })
}

方式3:路由组中间件

r := server.RouteGroup("/api")
r.Use(ctx.LogMiddleware)
r.Use(ctx.AuthMiddleware)

常用中间件示例

日志中间件

func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        logx.WithContext(r.Context()).Infow("request",
            logx.Field("method", r.Method),
            logx.Field("path", r.URL.Path),
            logx.Field("duration", time.Since(start).Milliseconds()),
        )
    }
}

鉴权中间件(自定义)

func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            httpx.WriteJson(w, http.StatusUnauthorized, map[string]string{"code": "401", "msg": "未登录"})
            return
        }
        // 解析 token,验证角色权限
        // ...
        next(w, r)
    }
}

CORS 跨域中间件

go-zero 提供三种内置 CORS 方案:

方案1:最简单

server := rest.MustNewServer(c.RestConf, rest.WithCors("http://localhost:8080"))

方案2:设置允许的 Headers

server := rest.MustNewServer(c.RestConf,
    rest.WithCors("http://localhost:8080"),
    rest.WithCorsHeaders("Content-Type,Authorization"),
)

方案3:自定义 CORS 处理

server := rest.MustNewServer(c.RestConf,
    rest.WithCustomCors(
        func(header http.Header) {
            header.Set("Access-Control-Allow-Origin", "*")
            header.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        },
        func(w http.ResponseWriter) {
            w.WriteHeader(http.StatusForbidden)  // 不允许跨域时的响应
        },
        "http://localhost:8080",
    ),
)

方案4:手写 CORS 中间件

func CorsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
        w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }
        next(w, r)
    }
}

🚨 生产环境不要用 *,明确指定允许的域名和 Header。

Rate Limit 中间件

func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    limiter := limit.NewTokenLimiter(100, 200)

    return func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            httpx.WriteJson(w, http.StatusTooManyRequests, map[string]string{
                "code": "429",
                "msg":  "请求过于频繁",
            })
            return
        }
        next(w, r)
    }
}

gRPC 拦截器

go-zero 支持 Unary 和 Stream 两类拦截器:

Unary 拦截器

// 日志拦截器
func LoggerInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    start := time.Now()
    resp, err := handler(ctx, req)

    logx.WithContext(ctx).Infow("RPC call",
        logx.Field("method", info.FullMethod),
        logx.Field("duration", time.Since(start).Milliseconds()),
    )
    return resp, err
}

错误转换拦截器

func ErrorInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

    resp, err := handler(ctx, req)
    if err != nil {
        causeErr := errors.Cause(err)     // 解包,获取根因
        if e, ok := causeErr.(*xerr.CodeError); ok {
            // 自定义错误码 → gRPC status
            logx.WithContext(ctx).Errorf("【RPC-ERR】%+v", err)
            err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
        } else {
            logx.WithContext(ctx).Errorf("【RPC-ERR】%+v", err)
        }
    }
    return resp, err
}

注册拦截器

// 在 main.go 中
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
    // 服务端实现注册
    user.RegisterUserServer(grpcServer, svcCtx)
})

// 添加 Unary 拦截器
s.AddUnaryInterceptors(
    LoggerInterceptor,
    ErrorInterceptor,
)

// 添加 Stream 拦截器
s.AddStreamInterceptors(StreamLoggerInterceptor)

客户端拦截器

// 创建 RPC 客户端时注册客户端拦截器
client := user.NewUser(zrpc.MustNewClient(c.UserRpcConf,
    zrpc.WithUnaryClientInterceptor(clientLogger),
))

中间件 vs 拦截器对比

特性 HTTP 中间件 gRPC 拦截器
作用域 REST API 服务 gRPC 微服务间通信
协议 HTTP/1.1, HTTP/2 gRPC (HTTP/2)
签名 func(next http.HandlerFunc) http.HandlerFunc func(ctx, req, info, handler) (resp, err)
注册方式 DSL 声明 / 编程式 AddUnaryInterceptors()
内置实现 JWT、CORS、熔断、降载 日志、错误转换
链式执行 洋葱模型(嵌套) 链式调用(顺序)

中间件设计建议

建议 说明
单一职责 每个中间件只做一件事(鉴权/日志/限流分离)
顺序敏感 鉴权 → 限流 → 日志 → CORS(按影响范围从内到外)
无状态优先 中间件尽量无状态,依赖通过 ServiceContext 注入
性能警惕 中间件在热路径上,避免重操作
错误快速返回 鉴权失败立刻返回,不要继续调用 next()
统一模板 团队统一用 API DSL 声明 + goctl 生成,避免手写导致不一致

常见陷阱

陷阱 说明
🚨 JWT 中间件 leeway=0 NTP 时钟漂移 > 1s 导致合法 Token 被拒,需要 jwt.WithLeeway(5s)
🚨 中间件未在 ServiceContext 注册 DSL 声明后需要手动在 ServiceContext 中初始化,否则编译不过
🚨 中间件顺序错误 JWT 鉴权必须放在需要用户信息的中间件之前
🚨 CORS 使用 * 生产环境必须明确指定域名
🚨 在中间件中 next() 后操作 Header next() 返回后响应已经写入,无法再修改响应头
🚨 RPC 拦截器中吞掉错误 拦截器必须把错误正确返回,否则调用方感知不到失败