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)衡量模块内部元素之间的关联程度。边界与内聚是一体两面的关系:

  • 高内聚:模块内部的类、函数、数据紧密围绕同一业务职责,修改原因单一。
  • 低耦合:模块之间通过窄接口通信,依赖最少。

边界设计的目标是:让内聚最大化,让耦合最小化。下图展示了不同内聚程度的模块边界表现:

边界与内聚关系示意图 高内聚模块 订单创建 订单查询 订单状态机 订单持久化 低内聚模块 订单创建 用户登录 日志记录 支付回调 高内聚:模块内所有元素围绕同一业务概念(订单),修改原因单一 低内聚:模块内混杂不相关职责(订单+用户+日志+支付),修改风险扩散 内聚度 ↓ 模块边界(接口契约) 对外暴露:CreateOrder(ctx, req) → (Order, error) 对外暴露:GetOrder(ctx, orderID) → (Order, error)

1.4 边界设计原则

在 Go 业务系统中,设计模块边界时应遵循以下原则:

  1. 单一职责原则(SRP):一个模块只对一个业务角色负责。例如“订单模块”不应同时处理“用户通知”。
  2. 接口隔离原则(ISP):客户端不应依赖它不需要的接口。将大接口拆分为多个小接口。
  3. 依赖反转原则(DIP):模块应依赖抽象(接口),而非具体实现。Go 的 interface 天然支持此原则。
  4. 稳定依赖原则(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 包,通过依赖注入获取实现。
• 边界清晰后,模块可独立测试、独立部署、独立演进。