type 定义是领域语义的起点,通过 type UserID string 创建专属类型以固化业务约束、支持方法扩展与构造校验,而非用 type UserID = string 别名导致语义丢失;聚合根用领域 ID 类型公开字段、私有状态封装与方法变更入口;值对象借私有字段+构造函数保障不可变性;跨上下文仅引用 ID 类型并定义在源 domain 包中,确保松耦合。

type 定义不是装饰,而是领域语义的起点。Go 里没有“领域类型”语法糖,所有聚合根、值对象、ID 类型都靠 type 显式声明 —— 这不是为了多写一行,而是把业务约束提前固化到类型系统里。
为什么必须用 type 而不是 type X = string
类型别名(=)和类型定义(type NewType OriginalType)行为完全不同:
- type UserID = string:只是 alias,和 string 完全互换,能传给任何接受 string 的函数,无法加专属方法,也拦不住误赋值
- type UserID string:全新类型,不能直接和 string 互传,可独立实现 Validate()、String(),还能在构造函数里做非空/格式校验
常见错误是用别名图省事,结果 userID := UserID("abc") 和 userID := "abc" 都合法,领域语义彻底丢失
type 怎么配合聚合根封装状态
聚合根结构体字段必须小写(私有),但 ID 必须公开 —— 这个矛盾靠 type 拆解:
- 公开 ID 字段用领域专用类型:ID UserID,而非 ID string
- 内部状态全小写:items []orderItem、status orderStatus,外部无法直接读写
- 所有变更入口只暴露方法:o.AddItem(item)、o.Confirm(),方法内部才操作私有字段
关键点在于:ID 类型本身就能携带校验逻辑(比如 func (id UserID) Validate() error),而 string 不行
值对象必须不可变,type 是第一道防线
值对象(如 Money、SKU、Address)强调“创建即冻结”,靠 type + 构造函数 + 私有字段实现:
- type Money struct{ amount int; currency Currency },字段全小写
- 没有 setter 方法,只有 NewMoney(amount int, currency Currency) (Money, error)
- 一旦构造完成,amount 和 currency 就不能再改 —— Go 没有 final 关键字,全靠纪律和类型设计守住
容易踩的坑是把值对象定义成 type Money = struct{...} 或暴露 public 字段,等于放弃不变性保障
跨限界上下文引用只存 ID 类型,不存结构体
限界上下文之间必须松耦合,引用方式只能是 ID:
- 库存服务里不能 import 订单的 Order struct,也不能存 order *domain.Order
- 只能存 orderID OrderID(自定义类型),且这个 OrderID 类型定义在订单 domain 包里
- 如果库存需要查订单信息,必须通过 OrderRepository.FindByID() 接口,由应用层协调
很多团队失败就卡在这一步:用 type OrderID string 但定义在 infra 层,或者直接用 string 传 ID,导致上下文边界形同虚设
真正难的不是写出 type OrderID string,而是让整个团队理解——这行代码不是格式要求,而是对业务规则的一次编码。一旦 ID 类型被滥用或绕过,聚合一致性、限界上下文、甚至事务边界都会无声瓦解。



















