单体架构是适用边界明确的合理选择,非过渡态;cmd/仅作初始化组装,按业务域而非技术层建包,internal/与pkg/严格划清可见性边界,go.mod须规范管理依赖。

单体架构不是过渡态,而是有明确适用边界的合理选择——只要业务模块边界清晰、团队规模适中、交付节奏优先,单体就是最省力且最难出错的起点。
cmd/ 下只放 main.go,别塞任何业务逻辑
很多新手在 cmd/main.go 里直接写路由注册、中间件链、DB 初始化,结果导致测试难写、复用难拆、多入口难维护。它唯一该做的事是“组装”和“启动”。
-
cmd/api/main.go只做四件事:解析命令行参数 → 加载internal/config→ 构建internal/server实例 → 调用.Run() - 所有中间件注册、路由绑定、健康检查端点,都下沉到
internal/server包内,用函数选项(functional option)注入依赖,比如server.WithMiddleware(...) - 如果后续要加 CLI 工具或定时任务入口,新建
cmd/cli/main.go或cmd/worker/main.go,复用同一套internal/逻辑,而不是复制粘贴main.go
按业务域建包,别按技术层建目录
看到 handlers/、services/、repositories/ 并列在项目根目录下,基本意味着半年后你会花 2 小时找一个用户登录相关的三处代码。这不是分层问题,是组织失焦。
- 每个业务功能自成一包:
user/、order/、payment/,包内再按职责拆文件,如user/service.go、user/repo.go、user/handler.go -
user/包对外只暴露UserService接口和CreateUserRequest类型,不导出数据库模型或中间件实现 - 跨业务调用必须走接口,禁止
import "myapp/internal/order/repo"这类直连内部实现的写法;如果user包 import 超过 3 个其他业务包,说明它已承担过多协调职责,该拆或重定义边界
internal/ 和 pkg/ 的可见性边界必须卡死
Go 的首字母大小写规则只能控制符号级导出,internal/ 是 Go 工具链强制的包级隔离机制——任何含 /internal/ 的路径,都无法被本项目以外的 module 导入。这点常被忽略,却决定了你未来能否安全地发布 SDK 或拆微服务。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
-
internal/放纯私有逻辑:比如internal/config、internal/middleware、internal/server,它们可被cmd/api和cmd/worker共用,但绝不允许外部项目import -
pkg/放真正可复用的公共能力:比如pkg/logger(不是pkg/common)、pkg/httpx(封装http.Client)、pkg/uuid,这些包要有完整单元测试和文档注释 - 一旦发现
pkg/下某个包被 3 个以上业务模块强依赖,就要警惕它是否在演变成“上帝工具包”,考虑按场景进一步拆分,比如把通用校验从pkg/util拆为pkg/validate和pkg/sanitize
go.mod 必须在项目根目录,且不设 replace 除非调试
有些团队为了快速验证某个依赖分支,在 go.mod 里写一堆 replace,结果上线前忘记删,CI 构建失败才意识到版本不一致。这不是临时方案,是埋雷行为。
-
go.mod必须位于项目根目录,且只描述本项目真实依赖;所有本地调试用的replace应该通过go mod edit -replace=...临时添加,并在提交前清理 - 新增包时,立刻运行
go list -m all | grep your-new-package验证是否被正确引入,避免隐式依赖或循环引用 - 如果依赖某模块的特定 commit,优先用
golang.org/x/tools@v0.18.0这种 tagged 版本,而非golang.org/x/tools@9f3e5a4这类 hash,后者无法被 proxy 缓存,CI 构建易失败
真正容易被忽略的,是业务包之间的依赖方向——它必须是单向的、可静态分析的。哪怕只是改一行日志格式,如果触发了 5 层包依赖重编译,说明结构已经失控。每次新增 import,都该问一句:这个依赖真的不可替代吗?还是只是因为没把共用逻辑提前抽到 pkg/?

















