值对象和实体的本质区别在于建模意图:前者描述“一个东西”,后者追踪“某个东西”;判断标准是是否需独立生命周期,ID字段或数据库主键不能作为充要条件。

值对象和实体不是命名风格或结构体字段多寡的问题,而是建模意图的分水岭:你是在描述“一个东西”,还是在追踪“某个东西”。
怎么判断一个结构体该定义成 Entity 还是 ValueObject
看它是否需要被独立追踪生命周期。如果业务上会说“这个订单的状态变了”“这个用户改名了”,那它大概率是 Entity;如果说“这个地址填错了”“这个金额算少了”,而没人关心“是哪个地址实例”,那它就是 ValueObject。
常见误判点:
- 把
ID字段当成实体的充要条件——type UserID string是值对象,type User struct{ ID UserID; Name string }才是实体 - 用数据库主键反推领域模型——MySQL 里每张表都有
id,但领域里“折扣规则”“配送时段”可能全是值对象 - 为图省事把所有结构体都加
ID——这会让值对象失去不可变性保障,也污染通用语言
ValueObject 必须不可变,但 Go 没有 final 关键字怎么办
Go 里靠约定 + 封装 + 构造函数强制约束。不提供 setter,只暴露构造函数和只读字段访问器。
正确写法示例:
type Money struct {
amount float64
currency string
}
<p>func NewMoney(amount float64, currency string) Money {
return Money{amount: amount, currency: currency}
}</p><p>func (m Money) Amount() float64 { return m.amount }
func (m Money) Currency() string { return m.currency }
</p>
错误写法(破坏不可变性):
- 导出字段:
Amount float64→ 外部可直接赋值 - 提供修改方法:
func (m *Money) SetAmount(...)→ 违反值对象语义 - 接收指针参数构造:
func NewMoney(...) *Money→ 容易被意外修改
为什么 Entity 的 ID 类型必须是自定义类型,不能用 int64 或 string
裸类型会导致类型混用、逻辑泄漏和测试脆弱。比如 user.ID == order.UserID 在编译期无法拦截,运行时才出错。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
推荐做法:
- 定义
type UserID string、type OrderID string,哪怕底层都是string - 所有 ID 类型实现
Stringer和UnmarshalText(方便日志和 HTTP 解析) - 在 repository 层统一生成 ID(如 UUID v4),禁止领域层调用
uuid.New()—— 领域层只关心“有 ID”,不关心“怎么来”
一个典型后果:没做类型隔离时,你可能在 User 方法里直接拼 SQL:"SELECT * FROM users WHERE id = " + u.ID,这等于把存储细节拖进了领域层。
聚合根里嵌套的 ValueObject 能否直接暴露给外部
可以,也应该。值对象天生适合共享和传递,只要它是不可变的。
但要注意边界:
- 不要暴露内部可变字段(比如返回
*Address指针) - 聚合根方法返回值对象应是副本或只读视图,例如:
func (c *Customer) Address() Address(而非*Address) - 若值对象较大(如含图片 Base64),考虑用 lazy loading 或 ID 引用代替直接嵌入
最常被忽略的一点:值对象的相等性必须重载 Equal 方法(或用 cmp.Equal),否则两个字段相同的 Address 实例用 == 比较会失败——因为 Go 结构体比较要求字段完全一致且不可含 map/slice/func,而值对象恰恰常含 slice(如 []string tags)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










