cmd目录只放main函数,业务逻辑下沉至internal/app及以下;internal按限界上下文分层(如user),每层职责自包含;pkg仅放真正通用、可跨项目复用的无业务代码;通过go mod tidy和构建验证守牢目录边界。

cmd 目录只放 main 函数,不放业务逻辑
很多人把 cmd 当成“启动入口集合”,结果往里塞了配置加载、服务注册、中间件组装——这会让多个微服务二进制无法复用核心逻辑,也破坏了构建时的可替换性。
实操建议:
-
cmd下每个子目录对应一个独立可执行程序(如cmd/user-service、cmd/order-service),每个只含一个main.go -
main.go里只做三件事:解析命令行参数、初始化全局依赖(如 logger、config)、调用internal/app.Run()启动 - 所有服务启动细节(gRPC server 创建、HTTP 路由注册、健康检查绑定)必须下沉到
internal/app或更下层 - 避免在
cmd中 importinternal/domain以外的业务包——否则编译单个 service 会拉入整个 domain 层
internal 目录按职责分层,不是按技术分层
常见错误是把 internal 拆成 internal/handler、internal/service、internal/repository 这种“技术栈分层”,结果 handler 直接 new repository,domain 模型被 HTTP 请求结构体污染。
正确做法是按业务能力切片,每片自包含完整职责链:
立即学习“go语言免费学习笔记(深入)”;
- 每个子目录(如
internal/user)代表一个限界上下文,内含domain/(实体、值对象、领域事件)、application/(用例、DTO、端口接口)、infrastructure/(实现端口的具体适配器,如 gorm repo、redis cache) -
internal/user/application只能依赖internal/user/domain和internal/user/infrastructure,禁止跨上下文 import(如internal/order/domain) - HTTP handler 放在
internal/user/infrastructure/http,它只调用application层导出的函数,不碰domain的具体实现 - 如果两个上下文需要通信,必须通过 domain event + async pub/sub,而不是直接调用对方的
application函数
pkg 目录只放真正可复用的、无业务语义的通用代码
误把 pkg 当成“工具函数垃圾桶”,塞进 UserValidator、OrderHelper 这类带业务名词的代码,会导致跨服务耦合,也违背 Go 的“少即是多”原则。
判断是否该进 pkg 的标准很硬:
- 能否脱离当前项目,在另一个完全无关的 Go 项目中直接 go get 使用?比如
pkg/trace(封装 OpenTelemetry SDK)、pkg/health(通用健康检查接口和 HTTP handler) - 是否不依赖任何
internal包?pkg层不能 importinternal/user/domain,也不能 importcmd - 是否稳定?频繁变更的代码(如某个 proto 生成的 client)不要放
pkg,应放在internal/xxx/infrastructure/grpc - 典型反例:
pkg/db如果只是封装了gorm.Open,没问题;但如果加了UserPreload()这种业务方法,立刻移走
go mod tidy 会暴露目录规划缺陷
很多团队直到上线前才发现 go mod tidy 报错:某个 internal 包被 pkg 引用,或者 cmd 间接依赖了测试用的 mock 工具。这不是模块系统的问题,是目录边界没守牢。
每次提交前快速验证:
- 运行
go list -f '{{.Deps}}' ./cmd/user-service,确认输出里没有internal/order或pkg/user这类越界依赖 - 在
pkg/trace目录下执行go build ./...,应成功——说明它真能独立编译 - 删掉
internal/user/infrastructure/kafka,看internal/user/application是否还能编译通过;如果失败,说明 application 层不该依赖 kafka 实现细节 - CI 中加一条
find internal -name 'go.mod' | xargs -I{} dirname {} | xargs -I{} sh -c 'cd {} && go list -f \"{{.Imports}}\" . | grep -q internal || echo \"error: {} imports nothing from internal\"',防止单个上下文空壳化
目录结构不是写完就一劳永逸的事,它是靠持续的 import 约束和构建验证来维持的。一旦松动,半年后你会在 internal 里看到 pkg 风格的 utils 文件夹,而没人记得当初为什么破例。










