中间件与拦截器
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 拦截器中吞掉错误 | 拦截器必须把错误正确返回,否则调用方感知不到失败 |