深入解析:Go+DDD 对象全家桶:DO/VO/DTO 等概念 + 实践

前言

本文面向在 Go 中实践领域驱动设计(DDD)的工程师,旨在清晰定义常见 “对象” 概念(DO/VO/DTO/PO/BO/AO/Form),并明确其目录位置、转换时机、最佳实践及反模式,同时提供查询条件容器的设计建议。

核心概念定义

名称全称核心特征建议存放位置
DODomain Object / Entity有唯一标识(ID)、生命周期与业务行为,包含领域规则domain 包 / 模块下
VOValue Object不可变、按值比较(无独立 ID),代表具体值 / 概念(如 Money、Address)domain/vo 或领域实体同包
DTOData Transfer Object用于层间数据传输(如 API↔Service),携带序列化标签(JSON/protobuf 等)api/dtoapplication/dto 或 transport 层
POPersistence Object与数据库映射(含 ORM/SQL 标签),属于仓储实现细节internal/repo/mysql/modelinfra/persistence
BOBusiness Object命名模糊(业务组合对象 / 服务层 “富 DTO”),建议避免;若使用需明确语义application/bo(限定为 “业务应用层只读聚合视图”)
AOApplication Object应用层内部传递对象,易与 DTO 混淆,建议复用 DTO 或明确命名application/* 或直接使用 DTO
Form-HTTP 层表单 / JSON 绑定结构体,带校验标签(binding/validate)api/handler/formtransport/http/form

设计总原则

  1. 领域模型纯净性domain 包内的 DO/VO 不得包含数据库 / JSON 标签,不得依赖基础设施(infra)包,仅保留业务逻辑与验证逻辑。
  2. 转换责任边界:DB 与 Domain、HTTP 与 Domain 的转换必须放在适配器层(repository/handler/mapper),领域层不处理格式转换。
  3. 依赖倒置:领域层定义抽象接口(如domain/repository),具体实现(如 ORM/PO)放在基础设施层(infra)。
  4. 命名唯一性:避免 BO/AO/DTO 混用,若需多用途,通过模块名明确区分(如api/dtoapplication/vo)。

推荐项目目录结构(Go)

/cmd/...                  # 程序入口
/internal/
app/                    # 应用层(用例/服务)
user_service.go       # 业务服务实现
dto/                  # 应用层DTO
user_dto.go
domain/                 # 领域层
user/
user.go             # DO(实体)
address_vo.go       # VO(值对象)
repository.go       # 仓储接口(领域层抽象)
repo/                   # 基础设施层(仓储实现)
mysql/
model/
user_po.go        # PO(数据库模型)
user_repo.go        # 仓储接口实现
mapper.go           # PO ↔ DO 映射函数
api/                    # 接口层
http/
handler/
user_handler.go   # HTTP处理器
form/
user_form.go      # Form(HTTP绑定结构体)
pkg/
query/                # 通用查询条件容器

典型代码示例

1. 领域层:DO(实体)+ VO(值对象)

DO(实体):封装业务行为与生命周期internal/domain/user/user.go

package user
import "time"
var (
ErrInvalidName  = errors.New("invalid name")
ErrInvalidEmail = errors.New("invalid email")
)
// User 领域实体(DO)——不含任何DB/JSON标签
type User struct {
ID        string
Name      string
Email     Email  // 嵌入VO
CreatedAt time.Time
}
// NewUser 领域工厂:确保实体创建符合业务规则
func NewUser(id, name string, email Email) (*User, error) {
if name == "" {
return nil, ErrInvalidName
}
if err := email.Validate(); err != nil {
return nil, err
}
return &User{
ID:        id,
Name:      name,
Email:     email,
CreatedAt: time.Now(),
}, nil
}
// ChangeEmail 领域行为:封装邮箱修改规则
func (u *User) ChangeEmail(e Email) error {
if err := e.Validate(); err != nil {
return err
}
u.Email = e
return nil
}

VO(值对象):不可变,按值比较,封装验证逻辑internal/domain/user/email.go

package user
// Email 值对象:代表邮箱概念,不可变
type Email struct {
Addr string // 实际邮箱地址
}
// NewEmail 工厂方法:确保创建时验证合法性
func NewEmail(addr string) (Email, error) {
e := Email{Addr: addr}
if err := e.Validate(); err != nil {
return Email{}, err
}
return e, nil
}
// Validate 验证逻辑:封装邮箱格式规则
func (e Email) Validate() error {
if e.Addr == "" {
return ErrInvalidEmail
}
// 实际场景可添加正则校验(如`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return nil
}

2. 基础设施层:PO(持久化对象)与映射

PO(数据库模型):与数据库字段映射,含 ORM 标签internal/repo/mysql/model/user_po.go

package model
import "time"
// UserPO 数据库映射对象(PO)——含GORM标签
type UserPO struct {
ID        string    `gorm:"primaryKey;column:id"`
Name      string    `gorm:"column:name;not null"`
Email     string    `gorm:"column:email;uniqueIndex"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
}
// TableName 显式指定数据库表名
func (UserPO) TableName() string {
return "users"
}

映射函数:负责 PO 与 DO 的转换(放在仓储实现层,避免领域层依赖 infra)internal/repo/mysql/mapper.go

package mysql
import (
"myapp/internal/domain/user"
"myapp/internal/repo/mysql/model"
)
// poToDomain 将PO转换为领域对象(DO)
func poToDomain(po *model.UserPO) (*user.User, error) {
email, err := user.NewEmail(po.Email) // 依赖领域VO的工厂方法,确保验证
if err != nil {
return nil, err
}
return &user.User{
ID:        po.ID,
Name:      po.Name,
Email:     email,
CreatedAt: po.CreatedAt,
}, nil
}
// domainToPO 将领域对象(DO)转换为PO
func domainToPO(do *user.User) *model.UserPO {
return &model.UserPO{
ID:        do.ID,
Name:      do.Name,
Email:     do.Email.Addr, // 提取VO的原始值
CreatedAt: do.CreatedAt,
}
}

3. 接口层:Form(HTTP 绑定)与 DTO(数据传输)

Form(HTTP 请求绑定):带校验标签,负责接收用户输入internal/api/http/form/user_form.go

package form
// CreateUserForm HTTP请求绑定结构体(Form)
type CreateUserForm struct {
Name  string `json:"name" binding:"required,min=2,max=30"`  // 表单校验规则
Email string `json:"email" binding:"required,email"`        // 使用gin的binding标签
}

DTO(应用层传输):定义服务间数据结构,带序列化标签internal/app/dto/user_dto.go

package dto
// UserDTO 应用层数据传输对象
type UserDTO struct {
ID    string `json:"id"`     // 序列化标签(供API返回)
Name  string `json:"name"`
Email string `json:"email"`
}
// CreateUserDTO 用于创建用户的DTO(输入参数)
type CreateUserDTO struct {
Name  string `json:"name"`
Email string `json:"email"`
}

4. 转换流程示例(Handler → Service → Repository)

Handler 层:Form → DTO 转换,调用服务internal/api/http/handler/user_handler.go

package handler
import (
"myapp/internal/api/http/form"
"myapp/internal/app"
"myapp/internal/app/dto"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userService *app.UserService
}
func (h *UserHandler) Create(c *gin.Context) {
// 1. 绑定HTTP请求到Form并校验
var f form.CreateUserForm
if err := c.ShouldBindJSON(&f); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 2. Form转换为应用层DTO
reqDTO := dto.CreateUserDTO{
Name:  f.Name,
Email: f.Email,
}
// 3. 调用应用服务
resDTO, err := h.userService.Create(c.Request.Context(), reqDTO)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 4. 返回DTO
c.JSON(200, resDTO)
}

Service 层:DTO → 领域对象 转换,调用领域逻辑internal/app/user_service.go

package app
import (
"context"
"myapp/internal/app/dto"
"myapp/internal/domain/user"
)
type UserService struct {
userRepo user.Repository // 依赖领域层仓储接口
}
func (s *UserService) Create(ctx context.Context, req dto.CreateUserDTO) (*dto.UserDTO, error) {
// 1. DTO转换为领域VO(通过领域工厂确保验证)
email, err := user.NewEmail(req.Email)
if err != nil {
return nil, err
}
// 2. 创建领域对象(DO)
u, err := user.NewUser(generateID(), req.Name, email)
if err != nil {
return nil, err
}
// 3. 调用仓储保存领域对象
if err := s.userRepo.Save(ctx, u); err != nil {
return nil, err
}
// 4. 领域对象转换为返回DTO
return &dto.UserDTO{
ID:    u.ID,
Name:  u.Name,
Email: u.Email.Addr,
}, nil
}

转换时机与位置规范

转换方向负责层 / 组件核心目的
HTTP Form → DTOHandler(transport 层)解析用户输入,做初步格式校验,隔离 HTTP 框架细节
DTO → 领域对象(DO/VO)Application(服务层)将外部数据转换为领域可识别的模型,通过领域工厂确保业务规则被遵守
领域对象 → DTOApplication(服务层)隐藏领域内部细节,仅暴露外部需知的字段,适配输出协议(如 JSON/protobuf)
PO ↔ DORepository 实现(infra 层)隔离数据库模型与领域模型,确保领域层不依赖 ORM / 数据库细节

查询条件容器(Query/Criteria)设计

查询条件容器用于封装查询参数(如分页、过滤、排序),其设计需避免耦合外部协议或数据库细节。

推荐方案

  1. **简单场景(与仓储强耦合)**在领域仓储接口层定义查询容器,确保上层(应用层)可直接使用:

    // domain/user/repository.go(领域仓储接口)
    type UserQuery struct {
    Name     string // 按名称过滤
    Email    string // 按邮箱过滤
    Page     int    // 分页参数
    PageSize int
    SortBy   string // 排序字段
    }
    type UserRepository interface {
    Find(ctx context.Context, q UserQuery) ([]*User, int64, error) // 返回数据+总数
    }
  2. HTTP 驱动的查询api/form中绑定 HTTP 参数,再转换为仓储层的查询容器(避免 HTTP 标签污染仓储接口):

    // api/http/form/user_query_form.go
    type UserQueryForm struct {
    Name     string `form:"name"`
    Email    string `form:"email"`
    Page     int    `form:"page" binding:"min=1"`
    PageSize int    `form:"page_size" binding:"min=1,max=100"`
    }
    // Handler中转换
    func (h *UserHandler) List(c *gin.Context) {
    var f form.UserQueryForm
    c.ShouldBindQuery(&f)
    // 转换为领域查询容器
    q := user.UserQuery{
    Name:     f.Name,
    Email:    f.Email,
    Page:     f.Page,
    PageSize: f.PageSize,
    }
    users, total, _ := h.userService.List(c, q)
    }
  3. **复杂过滤(规格模式)**若过滤逻辑涉及领域语义(如 “已支付且金额≥1000”),在领域层定义Specification接口:

    // domain/specification.go
    type Specification[T any] interface {
    IsSatisfiedBy(T) bool // 内存中验证
    ToSQL() (string, []interface{}) // 转换为SQL条件(可选,由仓储实现)
    }

最佳实践总结

  1. 领域层纯净domain包只放 DO/VO/ 接口和业务逻辑,不依赖任何外部包(如 ORM/HTTP 框架)。
  2. 单向依赖:上层(应用 / 接口)依赖领域层接口,领域层不依赖下层(infra)。
  3. 映射集中管理:转换函数集中在适配器层(handler/repo),避免散落在业务逻辑中。
  4. DTO 专用化:为不同场景定义专用 DTO(如CreateUserDTO/GetUserDTO),避免 “万能 DTO”。
  5. VO 不可变:VO 使用值类型,通过工厂方法创建,禁止直接修改字段(确保值语义)。
  6. 错误边界清晰:转换失败(如无效邮箱字符串→Email VO)需在应用层捕获并返回友好信息。

常见反模式(禁止做法)

  1. 领域对象带 ORM/JSON 标签:污染领域模型,导致领域层依赖 infra,违反依赖倒置。
  2. Handler 直接使用 PO:暴露数据库实现细节(如字段名、索引),增加 API 与 DB 的耦合。
  3. DTO 替代领域模型:在 DTO 中实现业务逻辑,导致领域规则分散,失去 DDD 核心价值。
  4. 结构体多职责:同一结构体同时作为 PO/DTO/Form(如既带gorm标签又带json标签)。
  5. SQL 逻辑侵入领域层:在domain包中写 SQL 片段或 ORM 查询,混淆业务与存储逻辑。
  6. 命名混乱:BO/AO/DTO 随意混用,无明确注释,增加团队沟通成本。

进阶建议

  • 映射工具:小型项目建议手写映射(可读性高、易调试);大型项目可使用代码生成工具(如go generate+ 自定义模板),避免反射工具(如mapstruct)带来的隐式错误。
  • 测试:为所有映射函数编写单元测试,覆盖字段映射、格式转换(如时间字符串→time.Time)、验证失败场景。

结语(自查清单)

  • domain包是否只包含 DO/VO/ 接口和纯业务逻辑?
  • PO 是否放在infra/repo下,且领域层不依赖 PO?
  • DTO/Form 是否带专属标签(JSON/binding),且与领域对象严格分离?
  • 转换逻辑是否集中在 handler 或 repo 实现层,未侵入领域层?
  • 查询容器是否避免了 HTTP/DB 细节的直接耦合?
  • 所有映射函数是否有单元测试?

遵循以上规范,可在 Go+DDD 实践中实现清晰的分层边界、低耦合的代码结构,提升系统可维护性。

我的小栈:https://itart.cn/blogs/2025/practice/ddd-go-objects-concepts-hands-on.html

posted @ 2025-12-04 17:04  gccbuaa  阅读(1)  评论(0)    收藏  举报