多模块开发必须用 internal/ 目录隔离;每个模块应独立注册 mvc.Application 并设路由前缀;模块间通信须通过接口契约,禁止直接 import;超15模块或跨团队时应物理拆分为独立 Go module。

多模块开发必须用 internal/ 目录隔离
不把 controller/service/model 移进 internal/,就谈不上“模块化”。Iris 的 MVC 本身不强制目录结构,但一旦 controller 文件数超过 8 个、或某个 service 文件里方法数超 12 个,说明业务已具备独立演进能力——此时若还堆在根目录下,main.go 会迅速膨胀,路由注册和依赖注入逻辑开始互相污染。
正确做法是:在项目根目录新建 internal/,再按业务域建子包,比如:
-
internal/user/(含user_controller.go、user_service.go、user_model.go) -
internal/post/(同理) -
internal/shared/(放跨模块工具、错误码、通用 DTO)
注意:internal/ 是 Go 官方约定的私有包路径,外部模块无法 import,天然防止越界调用。
每个模块需独立注册 mvc.Application
不能只用一个 mvc.New(app.Party("/")) 把所有控制器塞进去。模块之间应有明确的路由前缀和生命周期边界,否则中间件、依赖注入、错误恢复都会串行干扰。
示例:为 user 模块单独注册:
userAPI := app.Party("/api/v1/users")
mvc.New(userAPI).Handle(new(user.Controller))
关键点:
- 每个
Party对应一个业务域,前缀即模块标识 -
mvc.New()实例不共享,避免Register()注入的依赖被其他模块意外覆盖 - 模块间共用的 service(如日志、缓存)应在最外层
app上注册,再通过b.Dependencies().Add()显式注入到各模块的BeforeActivation
模块间通信只能走接口契约,禁止直接 import 其他 internal 包
比如 post 模块需要查用户信息,不能写 import "myapp/internal/user" 然后调 user.Service.GetByID()——这会制造强耦合,导致 user 模块升级时 post 必须同步改。
应该定义接口并由上层注入:
// internal/shared/user.go
type UserService interface {
GetByID(id int64) (*User, error)
}
// internal/post/post_controller.go
func (c *PostController) BeforeActivation(b mvc.BeforeActivation) {
b.Dependencies().Add(func() UserService {
return c.userSvc // 由模块初始化时传入
})
}
这样 post 只依赖抽象,具体实现可替换为本地 mock、HTTP client 或 gRPC stub,不影响编译和测试。
大型项目要物理拆分,别硬撑单仓库
当 internal/ 下模块数超 15 个,或团队分属不同业务线(如支付组、内容组),单仓库协作成本会陡增:PR 冲突频繁、CI 构建变慢、依赖更新牵一发而动全身。
此时应物理拆分:
- 每个模块变成独立 Git 仓库,发布为 Go module(如
github.com/myorg/user-service) - 主服务只保留
main.go和顶层路由聚合,其余全靠go get引入 - MVC 控制器退化为薄胶水层,核心逻辑下沉至各自 module 的
service包中
拆分后,user 模块可独立跑单元测试、独立部署、独立做数据库迁移——这才是“模块”该有的自治性,不是文件夹命名差异。
最容易被忽略的是模块边界处的错误处理:HTTP 错误码、重试策略、超时控制,这些必须在模块接入点(即 mvc.Application.Handle() 前)统一收口,而不是散落在各 Controller 的 ctx.StatusCode() 里。否则故障排查时,你永远不知道是哪个模块悄悄吞掉了 500。


















