「middleware 目录一般建议放哪里?没有专门类似 http 的目录吗?」—— 这篇把 go-zero 的目录约定、为什么没有 http 目录、middleware 该放哪、以及 HTTP 中间件怎么写怎么注册一次讲清。
两步结论:① 没有名为 http 的目录;② 中间件建议放 internal/middleware/,而「HTTP 层」其实由 internal/handler 承担。
handler(路由+薄壳)和 logic(业务),所以不需要一个笼统的 http 文件夹。goctl 生成的目录里也没有它。internal/middleware/。当 .api 里声明了中间件、或你手动建,这个目录就放所有 HTTP 中间件。RPC 的拦截器同理放这里或 internal/rpc/middleware。一句话:go-zero 把 HTTP 层拆进了 handler,把横切逻辑抽到了 middleware。所以「专门类似 http 的目录」—— 它的角色由 handler 扮演,而不是叫 http。
config / handler / logic / svc / types 一定会有;middleware 目录在你在 .api 里声明了 middleware: 时由 goctl 生成,否则你手动建一个即可。无论哪种,约定都指向 internal/middleware/。HTTP 中间件统一收口在 internal/middleware/,路由注册仍然在 internal/handler/routes.go。下面是一份标准骨架。
svcCtx 或具体 client。routes.go 负责「把哪些路由套上哪些中间件」。全局的用 server.Use(),路由级的用 rest.WithMiddlewares()。handler 或 logic。handler 应该是薄壳、logic 应该只关心业务;横切关注点(鉴权、日志、跨域)属于 middleware,分开放才好复用、好测试。go-zero 的 HTTP 中间件就是 rest.Middleware 类型:func(next http.HandlerFunc) http.HandlerFunc。约定写法是用一个结构体包一层,方便注入依赖。
// internal/middleware/jwt.go package middleware import ( "net/http" "github.com/zeromicro/go-zero/rest" ) // 需要依赖时,用结构体包一层,构造器注入 type JwtMiddleware struct { Secret string } func NewJwtMiddleware(secret string) *JwtMiddleware { return &JwtMiddleware{Secret: secret} } // Handle 的签名正好就是 rest.Middleware func (m *JwtMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if !m.valid(token) { // 校验 httpx.Error(w, errors.New("unauthorized")) return // 不放行 } // 把 userId 透传给下游(走 context,不是 svc) ctx := context.WithValue(r.Context(), "uid", uid) next(w, r.WithContext(ctx)) // 放行 } } // 不需要依赖的轻量中间件,可以直接是函数 func LogMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { logx.Infof("req %s %s", r.Method, r.URL.Path) next(w, r) } }
要点:中间件签名 = func(next) next。放行就调 next(w, r),拦截就直接写响应并 return。需要数据库/Redis/配置,就用结构体 + 构造器注入(和 svc 的依赖注入思路一致)。
两种注册方式:server.Use() 对所有路由生效;rest.WithMiddlewares() 只对某组路由生效。推荐在 routes.go 里集中管理。
// user.go (main) server := rest.MustNewServer(c.RestConf) defer server.Stop() // 所有请求都过一遍:访问日志 + 跨域 server.Use(middleware.LogMiddleware) server.Use(middleware.NewCorsMiddleware().Handle) handler.RegisterHandlers(server, svcCtx) server.Start()
// internal/handler/routes.go func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ {Method: http.MethodPost, Path: "/login", Handler: LoginHandler(serverCtx)}, {Method: http.MethodGet, Path: "/profile", Handler: ProfileHandler(serverCtx)}, }, // 只对这组路由套 JWT 鉴权 rest.WithMiddlewares([]rest.Middleware{ middleware.NewJwtMiddleware(serverCtx.Config.Auth.AccessSecret).Handle, }), ) // 登录接口不套 JWT(否则永远进不来),单独加 server.AddRoutes([]rest.Route{ {Method: http.MethodPost, Path: "/login", Handler: LoginHandler(serverCtx)}, }) }
不想手动改 routes.go?可以在 .api 的 @server 里声明 middleware:,goctl 生成时会在 routes.go 留好调用位,你只管实现。
// user.api @server( jwt: Auth middleware: AuthMiddleware # 声明:这组接口要过 AuthMiddleware ) service user-api { @handler profile get /profile } // goctl 生成后,routes.go 会写出引用;你实现 internal/middleware/authmiddleware.go // 命名约定:XxxMiddleware → NewXxxMiddleware().Handle
AuthMiddleware → internal/middleware/authmiddleware.go 里的 NewAuthMiddleware),否则编译报「未定义」。手动在 routes.go 里 WithMiddlewares 更直观可控,新手优先用后者。jwt: Auth 是 go-zero 内置的 JWT 中间件(配置里写 Auth 的 secret/expire 即可),不用自己写;只有「非标准」的横切逻辑(自定义 header 校验、租户解析等)才需要你手写 middleware。go-zero 的 RPC 层(zrpc)不叫 middleware,叫拦截器 interceptor。写法不同,但目录约定一样——放 internal/middleware/ 或 internal/rpc/middleware。
// 服务端拦截器(在 main 注册) s := zrpc.MustNewServer(c.RpcServerConf, func(g *grpc.Server) { user.RegisterUserServer(g, server.NewUserServer(ctx)) }, rpcserver.WithUnaryServerInterceptor(myInterceptor)) // 客户端拦截器:zrpc.WithUnaryClientInterceptor(...) // 拦截器签名(和 HTTP 中间件不一样) func myInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { // 前置:日志/鉴权/限流 resp, err := handler(ctx, req) // 放行 // 后置:统计/报错 return resp, err }
internal/middleware,注册在 handler/routesinternal/middleware 或 rpc 包,注册在 zrpc server/clientfunc(next http.HandlerFunc) http.HandlerFuncUnaryServerInterceptor(含 info/handler)server.Use / rest.WithMiddlewaresWithUnaryServerInterceptor / 客户端 WithUnaryClientInterceptor| 疑问 | 答案 |
|---|---|
| middleware 建议放哪? | 独立成包 internal/middleware/。HTTP 中间件放这;需要依赖就结构体 + 构造器注入(和 svc 思路一致)。别塞进 handler/logic。 |
| 有没有专门类似 http 的目录? | 没有名为 http 的目录。go-zero 用分层代替按协议命名:HTTP 层由 internal/handler(路由 + 薄壳)承担,横切逻辑抽到 middleware。所以「http 目录」的角色被 handler 扮演了。 |
| 中间件长啥样? | rest.Middleware 类型 = func(next http.HandlerFunc) http.HandlerFunc。放行调 next(w,r),拦截直接写响应 return。 |
| 怎么注册? | 全局 server.Use(mw);路由级 rest.WithMiddlewares([...], routes...)。鉴权类别套在登录接口上。 |
| 能在 .api 声明吗? | 能,@server(middleware: Xxx),goctl 留好调用位,你按约定命名实现。但 JWT 用内置的 jwt: Auth 即可,不用手写。 |
| RPC 呢? | RPC 不叫 middleware,叫拦截器(UnaryServerInterceptor),目录约定类似,注册在 zrpc server/client。 |
终极一句话:go-zero 没有 http 目录,但 HTTP 层由 handler 承担、横切逻辑收到 middleware。中间件就放 internal/middleware/,用 rest.Middleware 签名写、用 Use / WithMiddlewares 注册;RPC 那边对应物叫拦截器。