1. 模块边界概述:边界、契约与内聚
在大型 Go 业务系统中,模块边界是架构的第一道防线。没有清晰的边界,代码会迅速退化为“大泥球”——所有逻辑纠缠在一起,修改一处便牵动全局。本节从三个核心问题切入:什么是模块边界、为什么需要接口契约、边界与内聚如何协同。
1.1 什么是模块边界
模块边界是系统内部逻辑划分的“墙”。它定义了某个模块负责什么、不负责什么,以及如何与外部通信。在 Go 中,边界通常由 package 和 interface 共同表达:
- 物理边界:Go 的
package隔离了命名空间,非导出标识符(小写开头)对外不可见。 - 逻辑边界:通过
interface定义模块对外承诺的能力,隐藏内部实现细节。 - 部署边界:微服务架构中,每个服务是一个独立的进程边界,通过 RPC/HTTP 通信。
关键认知:边界不是“墙”死一切,而是有控制的开放。模块内部可以自由重构,只要对外接口不变,调用方就不受影响。
1.2 为什么需要接口契约
接口契约是模块边界上的正式约定。它明确规定了:
- 输入:参数类型、取值范围、前置条件。
- 输出:返回值类型、成功/失败状态、后置条件。
- 行为:幂等性、并发安全、超时语义。
没有契约的边界只是“墙”,有了契约的边界才是“接口”。下表对比了有/无契约的差异:
| 维度 | 无契约 | 有契约 |
|---|---|---|
| 修改影响 | 内部改动可能破坏调用方 | 只要契约不变,内部可自由重构 |
| 测试成本 | 需要集成测试覆盖所有调用方 | 可 mock 接口,独立测试模块 |
| 团队协作 | 依赖口头约定,容易遗漏 | 接口即文档,IDE 可自动校验 |
| 演进能力 | 重构风险高,倾向于不修改 | 支持版本化契约,平滑升级 |
Go 实践:在 Go 中,接口契约通常定义在独立的
contract 或 port 包中,与实现分离。例如 contract.OrderService 只包含方法签名,impl.OrderServiceImpl 负责具体逻辑。
1.3 边界与内聚的关系
内聚(Cohesion)衡量模块内部元素之间的关联程度。边界与内聚是一体两面的关系:
- 高内聚:模块内部的类、函数、数据紧密围绕同一业务职责,修改原因单一。
- 低耦合:模块之间通过窄接口通信,依赖最少。
边界设计的目标是:让内聚最大化,让耦合最小化。下图展示了不同内聚程度的模块边界表现:
1.4 边界设计原则
在 Go 业务系统中,设计模块边界时应遵循以下原则:
- 单一职责原则(SRP):一个模块只对一个业务角色负责。例如“订单模块”不应同时处理“用户通知”。
- 接口隔离原则(ISP):客户端不应依赖它不需要的接口。将大接口拆分为多个小接口。
- 依赖反转原则(DIP):模块应依赖抽象(接口),而非具体实现。Go 的
interface天然支持此原则。 - 稳定依赖原则(SDP):稳定的模块不应依赖不稳定的模块。将易变逻辑放在边界内部。
常见陷阱:不要为了“复用”而强行合并模块。两个看似相似的业务逻辑,如果变化方向不同,就应该放在不同的边界内。例如“普通订单”和“预售订单”虽然结构相似,但状态机、库存策略完全不同,应拆分为两个模块。
1.5 边界在 Go 代码中的体现
以下是一个典型的 Go 模块边界代码结构:
// contract/order.go —— 接口契约(边界定义)
package contract
import "context"
type OrderService interface {
CreateOrder(ctx context.Context, req CreateOrderReq) (*Order, error)
GetOrder(ctx context.Context, orderID string) (*Order, error)
CancelOrder(ctx context.Context, orderID string) error
}
type CreateOrderReq struct {
UserID string
Items []OrderItem
Address string
}
type Order struct {
ID string
UserID string
Status string
Total float64
CreatedAt int64
}
// impl/order_service.go —— 内部实现(边界内部)
package impl
import (
"context"
"your-project/contract"
)
type orderServiceImpl struct {
repo OrderRepository
}
func NewOrderService(repo OrderRepository) contract.OrderService {
return &orderServiceImpl{repo: repo}
}
func (s *orderServiceImpl) CreateOrder(ctx context.Context, req contract.CreateOrderReq) (*contract.Order, error) {
// 内部实现细节:校验、计算、持久化
// 调用方只依赖 contract.OrderService 接口
}
要点总结:
• 接口契约定义在
• 实现包
• 调用方只依赖
• 边界清晰后,模块可独立测试、独立部署、独立演进。
• 接口契约定义在
contract 包,不依赖任何实现细节。• 实现包
impl 依赖 contract,但 contract 不依赖 impl。• 调用方只依赖
contract 包,通过依赖注入获取实现。• 边界清晰后,模块可独立测试、独立部署、独立演进。