「MVC 目录怎么分?model / controller(handler) / service(logic) 各放哪?Middleware 目录啥规范?文件要不要 _logic / _model / _handler 后缀?」一篇讲清 go-zero 与主流 Go 框架的差异与踩坑。
三条最关键结论,先记住再展开。
① Go 没有语言级/官方 MVC 强制。所谓「MVC」在 Go Web 里其实是分层架构:接入层(Handler/Controller)→ 业务层(Logic/Service)→ 数据层(Model/DAO)。
② 术语被「翻译」了。因为 Go 标准库叫 http.Handler,Go 圈更习惯说 Handler 而非 Controller;业务层 go-zero 叫 logic(其他框架常叫 service);数据层叫 model。
③ 文件后缀分两派。go-zero 用 goctl 代码生成,文件必须带 handler.go / logic.go / middleware.go / model.go 角色后缀(注意:没有下划线,是 loginhandler.go);Gin/Echo/手写 不强制后缀,目录/包名已表意,后缀只是团队风格。
同一个职责,不同框架叫法不同;搞清楚映射才不会被名词绕晕。
| MVC 角色 | 职责 | go-zero | 其他 Go 框架(Gin/Echo/Fiber) | 标准库 net/http |
|---|---|---|---|---|
| Controller | 接收请求、解析参数、返回响应 | handler / | controllers/ 或 handlers/ | handler 函数(无目录约定) |
| Service / Logic | 业务逻辑、编排 | logic / | services/ 或 service/ | 自定(常叫 service 包) |
| Model | 数据结构 + 数据访问(DAO) | model / | models/ 或 repository/ 或 store/ | 自定(常叫 model/store) |
| View | 渲染输出 | types/(DTO)+ JSON 响应;无模板层 | 若 SSR 用 templates/,否则也是 JSON | 自定 |
| Middleware | 横切逻辑(鉴权/日志/跨域) | middleware / | middleware/ | 手写中间件函数 |
http.Handler / http.HandlerFunc,路由框架(Gin/Echo/go-zero)都围绕它设计。所以「控制器的动作」在 Go 里天然就是「一个 Handler 函数」。go-zero 直接把目录叫 handler。Middleware → Handler(薄壳)→ Logic(业务)→ Model/DAO(数据)→ DB。箭头即目录边界。
httpx.Parse 解析、调 logic、httpx.OkJson 返回。不放业务。svcCtx 拿依赖,编排 model 调用,可独立单测。API 服务:handler / logic / svc / types / config / middleware;数据层 model 由 goctl model 生成。RPC 服务对应 server / logic / svc。
| 目录 | 对应 MVC 角色 | 装什么 | 谁生成 |
|---|---|---|---|
handler/ | Controller | 路由+薄壳,调 logic、写 JSON | goctl(按 .api) |
logic/ | Service | 业务逻辑,消费 svcCtx | goctl(按 .api) |
types/ | View(DTO) | 请求/响应结构体 | goctl(按 .api) |
svc/ | — | 依赖装配(组合根) | goctl |
config/ | — | 配置类型 | goctl |
middleware/ | Middleware | HTTP 中间件 / RPC 拦截器 | 手写 / .api 声明 |
model/ | Model(DAO) | 表模型 + 数据访问方法 | goctl model mysql |
.api 生成;数据层 model 由 goctl model mysql ddl 生成(带 _gen.go 后缀,见下节)。两者通过 svc 注入串起来。goctl 的命名模板是「接口名 + 角色名 + .go」。所以文件是 loginhandler.go,不是 login_handler.go。
| 目录 | 文件后缀(goctl 约定) | 示例 | 说明 |
|---|---|---|---|
handler/ | handler.go | loginhandler.go, profilehandler.go | 每个 @handler 生成一个 |
logic/ | logic.go | loginlogic.go, profilelogic.go | 与 handler 一一配对 |
middleware/ | middleware.go | jwtmiddleware.go, corsmiddleware.go | 也可写成 jwtmw.go,团队统一即可 |
model/ | model.go + model_gen.go | usermodel.go, usermodel_gen.go | _gen 是代码生成、可覆盖;手写扩展放 usermodel.go |
types/ | 通常 types.go | types.go | 所有 DTO 集中一处 |
svc/ | servicecontext.go | servicecontext.go | 固定文件名,goctl 必生成 |
_handler / _logic / _model 带下划线的写法,在 go-zero 里并不存在——goctl 生成的是连写的 loginhandler.go。带下划线的 login_handler.go 是 Gin 等手写项目的常见风格(见第 6、7 节),两派别混。_gen.go vs 手写扩展),把 handler/logic 按接口名配对,后缀就是这套机制的命名锚点。手动加文件时,建议沿用同后缀,否则 goctl 再生成时容易冲突或你 grep 不到。这些框架不生成代码,所以目录是「团队约定」,没有 go-zero 那种固定骨架。下面是一份社区常见的落地方式。
models/(Gin 常见)、repository/(DDD)、store/(标准库风)、dao/(Java 味)。语义相同。wire、fx,或简单的「App 结构体挂依赖」在 main 装配;没有 go-zero 的 svc 固定位。controllers/(沿用 MVC 词);纯标准库/Go 味项目称 handlers/。两者等价。这是你最关心的问题。答案:取决于你是否用代码生成工具。
| 场景 | 要不要后缀 | 原因 |
|---|---|---|
| go-zero(goctl 生成) | 要,且是硬约定 | goctl 靠后缀定位/配对/重生成;loginhandler.go / loginlogic.go。手写新文件也建议沿用,避免 goctl 再生成时冲突。 |
| Gin/Echo/手写 | 不强制 | 目录/包名已经表意(package handler 里的 user.go 一看就是 handler)。后缀是可选风格。 |
| 混合(手写也想要后缀) | 可以加 | 若同 feature 跨层文件多、想按名聚合或方便 grep,可写成 user_controller.go / user_service.go / user_model.go。纯团队审美,非强制。 |
Go 官方倾向:不加冗余后缀。因为「包名已经表达了角色」——handler/user.go 比 handler/user_handler.go 更符合 Go 命名哲学(见《Go 命名规范》篇:包名即前缀,别再带角色词)。但代码生成工具是例外:它必须用稳定后缀来锚定文件。
不管哪个框架,横切逻辑都建议独立成一个包,不要塞进 handler 或 logic。
| 框架 | 目录 | 文件/命名约定 | 注册方式 |
|---|---|---|---|
| go-zero | internal/middleware/ | jwtmiddleware.go(类型 JwtMiddleware + Handle 方法) | server.Use() 全局 / rest.WithMiddlewares() 路由级 / .api 声明 |
| Gin | middleware/ | jwt.go(返回 gin.HandlerFunc) | r.Use(...) 全局 / r.Group().Use() 路由级 |
| Echo | middleware/ | jwt.go(echo.MiddlewareFunc) | e.Use(...) / g.Use(...) |
| 标准库 | middleware/ | auth.go(func(next http.Handler) http.Handler) | 用 alice 链式或手动嵌套 |
rest.Middleware = func(next http.HandlerFunc) http.HandlerFunc;RPC 侧对应物叫拦截器 Interceptor(UnaryServerInterceptor),目录约定相同。鉴权类优先用内置 jwt: Auth,只有自定义横切逻辑才手写。middleware/,才好复用、好测试、好按需挂载到不同路由组。loginhandler.go / loginlogic.go / usermodel_gen.go,无下划线;手写新文件沿用同后缀。_gen.go 是生成物,改表后会被覆盖;自定义方法放同目录非 _gen 文件。| 职责 | go-zero 目录 | 其他框架目录 | go-zero 文件后缀 | 是否强制 |
|---|---|---|---|---|
| 接入层 | handler/ | controllers/ 或 handlers/ | xxxhandler.go | go-zero 强制;其他可选 |
| 业务层 | logic/ | services/ 或 service/ | xxxlogic.go | go-zero 强制;其他可选 |
| 数据层 | model/ | models/ repository/ store/ dao/ | xxxmodel.go / _gen.go | go-zero 强制;其他可选 |
| DTO/响应 | types/ | dto/ requests/ | types.go | go-zero 固定;其他随意 |
| 横切逻辑 | middleware/ | middleware/ | xxxmiddleware.go | go-zero 强制;其他可选 |
| 依赖装配 | svc/ | main / wire / fx / container | servicecontext.go | go-zero 固定位置 |
| 配置 | config/ + etc/ | config/ | config.go | go-zero 固定;其他随意 |
· go-zero 的 svc 目录结构 —— 依赖装配「组合根」详解
· go-zero middleware 放哪 —— 中间件目录与 HTTP/RPC 写法
· go-zero 服务启动 —— main 到端口监听
· Go 命名与目录结构规范全解 —— 文件/包/变量命名红线(含「包名即前缀,别带角色词」)