应按业务能力而非技术层划分包,如将订单相关校验、状态机、通知等全置于com.example.order包内,通过接口隔离依赖,禁止隐式循环引用,并严格区分internal/与pkg/职责。

按业务能力而非技术层划分包
把代码塞进 controller、service、repository 这类通用目录,看似整齐,实则让订单逻辑散落在三个包里,改个状态流转就得跨包跳转。真正的高内聚,是让所有跟“订单”相关的东西——校验规则、状态机、通知策略、补偿逻辑——都待在 com.example.order 包里。
常见错误现象:order.Service 依赖 payment.Service,而 payment.Service 又反向调用 order.Repo,形成隐式循环依赖,编译不报错但运行时出问题。
- 每个业务包只暴露接口(如
OrderService接口),不暴露结构体或实现细节 - 包内可自由分层(如
internal/state、internal/event),但对外只提供统一入口 - 避免跨包直接使用对方的 struct 字段;需要共享数据时,定义专用 DTO 或通过接口传参
用接口隔离模块间依赖
订单完成要触发支付,但订单模块绝不能 import 支付的具体实现。否则改支付宝对接逻辑,就得重新编译整个订单服务。
正确做法是:在 com.example.order 包里定义 PaymentNotifier 接口,由支付模块实现它,并在启动时注入。
立即学习“go语言免费学习笔记(深入)”;
- 接口定义放在调用方包内(即订单包),体现“我需要什么”,而非“你能提供什么”
- 实现方包(如
com.example.payment)只依赖接口定义包,不反向依赖调用方 - 注入时机通常在
cmd/main.go中组装,而不是在业务逻辑里 new 实例
internal/ 与 pkg/ 的职责必须划清
internal/ 下的包(如 internal/order)不是“还没想好要不要开源”,而是设计上就禁止外部 import——Go 编译器会强制检查。一旦你发现某个 internal/xxx 被其他项目 import 了,说明边界已破,要么重构为 pkg/,要么补足抽象层。
容易踩的坑:pkg/util 逐渐变成“什么都能塞”的垃圾桶,最后出现 util.OrderHelper 和 util.PaymentHelper,反而增加耦合。
-
pkg/只放真正通用、无业务语义的代码:日志封装、配置解析、错误码工具 -
internal/是业务逻辑主战场,可包含领域模型、应用服务、基础设施适配器 - 跨
internal/子包调用没问题,但禁止从pkg/反向 importinternal/
避免包名与目录名不一致的陷阱
Go 不允许包名和物理路径冲突。比如你在 internal/order 目录下写了 package payment,Go 会允许编译,但 IDE 无法跳转、go list 识别混乱、别人读代码时完全迷失。
更隐蔽的问题是:同一模块下多个包用了相同名字(如 internal/order/api 和 internal/order/core 都声明 package order),会导致符号冲突,尤其是当它们被不同 import 路径引用时。
- 包名必须与所在目录名完全一致(小写、无下划线、无驼峰)
- 目录名应反映其职责,而不是技术分类(不用
handler,改用httpapi或grpcserver) - 如果一个目录下逻辑确实复杂,拆子目录,而不是靠同名包硬撑


















