分布式事务画布Go领域建模与架构设计 - 第2节
2.1 Go 语言核心特性回顾
Go 语言(Golang)以其简洁的语法、强大的并发模型和高效的编译速度,成为构建分布式系统与微服务的首选语言之一。在进入领域驱动设计(DDD)实战之前,我们需要回顾几个对 DDD 实现至关重要的核心特性。
2.1.1 结构体与方法:实体与值对象的基石
Go 没有类(Class),而是通过 struct 和 method 实现封装。结构体用于聚合数据,方法用于定义行为,这天然契合 DDD 中“将数据与行为绑定”的聚合根与实体设计原则。
// 定义一个实体:用户
type User struct {
ID string
Name string
Age int
}
// 为实体绑定行为方法
func (u *User) ChangeName(newName string) {
u.Name = newName
}
2.1.2 接口:定义领域契约
Go 的接口是隐式实现的,只要类型实现了接口的所有方法,即视为实现了该接口。这一特性使得领域层可以定义抽象的 Repository 或 Service 接口,而将具体实现(如数据库操作)推迟到基础设施层,完美支持依赖倒置原则。
// 领域层定义接口
type UserRepository interface {
Save(user *User) error
FindByID(id string) (*User, error)
}
// 基础设施层实现接口
type MySQLUserRepository struct {
db *sql.DB
}
func (r *MySQLUserRepository) Save(user *User) error {
// 具体实现
return nil
}
2.1.3 并发原语:处理分布式事务的利器
Go 的 goroutine 和 channel 为处理高并发场景提供了轻量级方案。在分布式事务中,Saga 模式的协调器或 TCC 模式的 Try/Confirm/Cancel 调用,都可以借助 goroutine 实现异步编排,利用 channel 或 sync.WaitGroup 进行结果聚合。
2.2 领域驱动设计四层架构
DDD 经典的四层架构将系统划分为:用户接口层(Interface)、应用层(Application)、领域层(Domain) 和 基础设施层(Infrastructure)。每一层都有明确的职责和依赖方向:上层依赖下层,下层不依赖上层。
2.2.1 各层职责详解
| 层次 | 核心职责 | 典型组件 | 依赖方向 |
|---|---|---|---|
| 用户接口层 | 处理 HTTP/gRPC 请求、数据展示、用户会话 | Controller、DTO、View、Assembler | 依赖应用层 |
| 应用层 | 编排用例流程、事务控制、权限校验 | Application Service、Event Publisher | 依赖领域层 |
| 领域层 | 核心业务逻辑、领域规则、模型状态管理 | Entity、Value Object、Aggregate、Domain Service、Repository Interface | 无依赖(最核心) |
| 基础设施层 | 技术实现:数据库、消息队列、缓存、外部 API | Repository Impl、ORM、MQ Client | 实现领域层接口 |
internal/interface、internal/application、internal/domain、internal/infrastructure。领域层不应导入任何基础设施层的包。
2.3 实体与值对象在 Go 中的实现
实体(Entity)和值对象(Value Object)是领域模型中的两个基本构造块。理解它们的区别并在 Go 中正确实现,是构建高质量领域模型的第一步。
2.3.1 实体:具有唯一标识且可变的对象
实体拥有一个贯穿生命周期的唯一标识(ID),其属性可以变化,但身份不变。在 Go 中,通常使用结构体表示实体,并通过 == 或自定义方法比较 ID 来判断相等性。
// 订单实体
type Order struct {
ID string // 唯一标识
CustomerID string
TotalAmount float64
Status OrderStatus
CreatedAt time.Time
}
// 判断两个订单实体是否相同(基于 ID)
func (o *Order) Equals(other *Order) bool {
return o.ID == other.ID
}
// 修改实体状态
func (o *Order) Confirm() error {
if o.Status != OrderStatusPending {
return errors.New("only pending order can be confirmed")
}
o.Status = OrderStatusConfirmed
return nil
}
2.3.2 值对象:不可变且无标识的对象
值对象描述事物的属性,没有唯一标识,通过其所有属性值来判断相等性。一旦创建,不应被修改(不可变)。在 Go 中,可以通过不暴露修改方法、使用指针接收者返回新实例等方式实现不可变性。
// 地址值对象
type Address struct {
Province string
City string
District string
Detail string
}
// 值对象比较:比较所有字段
func (a Address) Equals(other Address) bool {
return a.Province == other.Province &&
a.City == other.City &&
a.District == other.District &&
a.Detail == other.Detail
}
// 不可变:返回新的 Address 实例
func (a Address) ChangeDetail(newDetail string) Address {
return Address{
Province: a.Province,
City: a.City,
District: a.District,
Detail: newDetail,
}
}
2.3.3 实体与值对象的对比
| 维度 | 实体 (Entity) | 值对象 (Value Object) |
|---|---|---|
| 唯一标识 | 有(ID 字段) | 无 |
| 可变性 | 可变(属性可更新) | 不可变(替换而非修改) |
| 相等性判断 | 基于 ID | 基于所有属性值 |
| 生命周期 | 独立生命周期 | 依附于实体 |
| Go 实现要点 | 暴露修改方法;ID 字段不可变 | 不暴露 setter;提供 Equals 方法 |
| 典型例子 | 用户、订单、产品 | 地址、金额、颜色、时间段 |
2.3.4 实战中的选择原则
- 优先使用值对象: 如果一个概念没有独立生命周期,且只用于描述属性(如金额、地址、时间段),优先设计为值对象。这能减少实体数量,降低模型复杂度。
- 实体只保留核心身份: 实体的 ID 一旦生成不应修改。将可变的、描述性的属性尽量委托给值对象。
- 利用 Go 的零值简化: 值对象可以使用零值表示“空”或“未设置”,避免额外的 nil 判断。
2.4 本章小结
本章回顾了 Go 语言中与 DDD 实现密切相关的核心特性:结构体与方法、接口、并发原语。深入讲解了 DDD 四层架构的职责划分与依赖规则,并通过代码示例和对比表格,明确了实体与值对象在 Go 中的实现差异。掌握这些基础,将为后续实现聚合、仓储和领域事件打下坚实的地基。