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

2025/11/6 实践总结最佳

# 前言

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

# 核心概念定义

名称 全称 核心特征 建议存放位置
DO Domain Object / Entity 有唯一标识(ID)、生命周期与业务行为,包含领域规则 domain 包 / 模块下
VO Value Object 不可变、按值比较(无独立 ID),代表具体值 / 概念(如 Money、Address) domain/vo 或领域实体同包
DTO Data Transfer Object 用于层间数据传输(如 API↔Service),携带序列化标签(JSON/protobuf 等) api/dtoapplication/dto 或 transport 层
PO Persistence Object 与数据库映射(含 ORM/SQL 标签),属于仓储实现细节 internal/repo/mysql/modelinfra/persistence
BO Business Object 命名模糊(业务组合对象 / 服务层 "富 DTO"),建议避免;若使用需明确语义 application/bo(限定为 "业务应用层只读聚合视图")
AO Application 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 典型代码示例

# 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 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"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

映射函数:负责 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,
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 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标签
}
1
2
3
4
5
6
7

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"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 转换时机与位置规范

转换方向 负责层 / 组件 核心目的
HTTP Form → DTO Handler(transport 层) 解析用户输入,做初步格式校验,隔离 HTTP 框架细节
DTO → 领域对象(DO/VO) Application(服务层) 将外部数据转换为领域可识别的模型,通过领域工厂确保业务规则被遵守
领域对象 → DTO Application(服务层) 隐藏领域内部细节,仅暴露外部需知的字段,适配输出协议(如 JSON/protobuf)
PO ↔ DO Repository 实现(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) // 返回数据+总数
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  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)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
  3. 复杂过滤(规格模式)若过滤逻辑涉及领域语义(如 "已支付且金额≥1000"),在领域层定义Specification接口:

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

# 最佳实践总结

  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 实践中实现清晰的分层边界、低耦合的代码结构,提升系统可维护性。