go中不推荐硬套mvc,因其违背“小接口、组合优先”哲学,易致职责混乱;应采用handler→service→repository分层,强调纯业务逻辑与框架解耦,通过interface注入实现可测试性与存储替换灵活性。

MVC 在 Go 语言中不是原生支持或被官方推荐的设计模式,它更像是一种从其他语言(如 Ruby on Rails、Spring MVC)迁移过来的组织惯性,直接套用反而容易导致结构臃肿、职责错位。
Go 项目里硬套 MVC 为什么容易出问题
Go 的哲学强调“小接口、组合优先、显式依赖”,而传统 MVC 中控制器(Controller)常沦为大杂烩:既处理 HTTP 参数解析、又调用业务逻辑、还拼接响应,违背单一职责;模型(Model)若照搬 ActiveRecord 模式(比如用 GORM 的 struct 同时承担数据映射、校验、业务方法),会迅速变得不可测试、难以复用。
- HTTP handler 层本应极薄,但 MVC 下常被塞进大量非路由逻辑
-
controller包名看似清晰,实则掩盖了领域边界模糊的问题 - 数据库层与业务逻辑强耦合,换存储(比如从 PostgreSQL 切到 Redis 缓存主读)时牵一发而动全身
- 测试时不得不启动 HTTP server 或 mock 整个请求生命周期,单元测试成本陡增
更符合 Go 习惯的分层思路:handler → service → repository
这不是新发明,而是把关注点切得更贴近 Go 的运行时现实:HTTP 是一种传输协议,不是架构分界线。核心逻辑应独立于框架存在。
-
handler只做三件事:解析请求(c.Param/c.ShouldBind)、调用service方法、构造响应(c.JSON) -
service包含纯业务逻辑,不依赖net/http或任何框架类型,输入输出都是普通 struct 和 error -
repository接口定义数据操作契约(如FindUserByID(ctx, id) (*User, error)),具体实现(gormRepo/memoryRepo)可自由替换 - 所有跨层依赖通过 interface 注入,避免
import循环,也便于单元测试时用内存实现替代真实 DB
cmd 和 internal 目录该怎么用才不踩坑
Go Modules 下,目录结构不是装饰,而是约束依赖流向的物理手段。用错位置,模块边界就失效。
立即学习“go语言免费学习笔记(深入)”;
-
cmd/xxx下只放main.go和极简初始化代码(flag.Parse、log.SetOutput、http.ListenAndServe),绝不放业务逻辑 -
internal/是真正的“内部包”,外部模块无法 import;internal/handler、internal/service、internal/repository各司其职,彼此可引用,但不能反向依赖cmd或pkg - 需要被外部复用的工具函数、通用类型、客户端封装,才放进
pkg/;否则一律沉到internal里 - 如果项目要拆微服务,
internal下的子目录天然对应不同服务边界,比model/controller/view更易切割
真正难的不是画出多漂亮的分层图,而是每次写一个新 handler 时,能忍住不把校验逻辑塞进它、不直接 new 一个 DB 实例、不为了“快”而绕过 service 层——这些瞬间的妥协,半年后都会变成重构时最硬的骨头。










