业务逻辑必须收进 domain 包,即订单校验、库存扣减、状态流转等规则应封装在 domain/order 的 Order 结构体及方法中;application 层仅编排调用,infrastructure 层只负责落地,跨模块依赖接口而非具体实现,包名按业务能力组织并用 internal 隔离实现,测试只 mock 小接口。

业务逻辑必须收进 domain 包,别塞进 handler 或 service
Go 项目里最常见的耦合源头,就是把订单校验、库存扣减、状态流转这些规则,直接写在 handler 或 service 里。结果一改需求,三四个包都要动,测试用例全挂。
正确做法是让 domain 包承载全部业务语义:
-
domain/order里定义Order结构体、Place()方法、Cancel()规则,所有校验逻辑都在结构体内完成 -
application层只做编排:调用order.Place(),再发领域事件,不掺和“能不能下单”这种判断 -
infrastructure层只管落地:比如把order存到 MySQL,或发消息到 Kafka,不碰业务规则
跨模块调用必须依赖接口,禁止 import 具体实现包
订单服务要通知支付成功,但不能直接 import "payment/internal" —— 这会把支付的具体实现(比如用支付宝还是微信)硬编码进订单逻辑里,一换支付渠道就得改订单代码。
应该这样解耦:
立即学习“go语言免费学习笔记(深入)”;
- 在
payment/api定义Notifier接口:NotifyOrderPaid(orderID string) error - 订单服务的
Complete()方法接收payment.Notifier作为参数,只调用接口方法 - 启动时由
main或 wire 注入具体实现,比如wechatNotifier或alipayNotifier
编译期就能断开强依赖,支付模块删了也不影响订单编译。
模块边界靠包名和导入路径显式表达,别靠注释或文档
Go 没有命名空间,包名就是契约。很多团队用 controller、service、dao 这种技术分层命名,结果订单逻辑散落在 order/controller、user/service、inventory/dao 里,根本看不出“下单”这件事到底涉及哪些模块。
推荐按业务能力组织包路径:
-
com.example.order—— 所有订单相关类型、规则、事件 -
com.example.payment—— 支付网关、回调处理、对账逻辑 -
com.example.common—— 仅放ID、Money等值对象,不放任何业务逻辑
每个模块对外只暴露 api 子包(如 order/api),其他内部实现包用 internal 标记,防止被外部 import。
测试时用接口 mock,但别 mock 整个模块
写单元测试时,常见错误是给整个 payment 包打桩,然后测 order.Complete()。这既慢又脆弱——只要支付接口加个字段,所有订单测试全崩。
真正该 mock 的,只是它依赖的那个小接口:
- 为
payment.Notifier写一个轻量 mock:type mockNotifier struct{ called bool } - 测试只验证
order.Complete()是否调用了NotifyOrderPaid(),不关心支付怎么发消息 - 支付模块自己的集成测试,再单独跑真实网关调用
高内聚低耦合的终极检验,不是看目录多整齐,而是当你删掉 payment 包、只留接口定义时,order 仍能通过编译并跑通核心单元测试。


















