返回首页

Go 中的 DDD 和 Clean Architecture:务实方法

本文解释了如何务实地在 Go 中应用 DDD 和 Clean Architecture,避免过度结构化。提供了代码示例、项目组织推荐以及减少技术债务的关键规则。

Go:无需 200 个文件的 DDD 和 Clean Architecture
Advertisement 728x90

Go 中的 DDD 和 Clean Architecture:实用方法,避免过度工程化

源于架构问题的技术债消耗了公司 IT 预算的 40%。在 Go 项目中,这常常表现为过度复杂的结构:200 个文件塞满了空接口和层层嵌套的逻辑,业务逻辑被深埋其中。根本原因是错误应用了从 Java 或 C# 借来的 DDD 和 Clean Architecture 模式。但如果实用地使用,这些模式能有效解决复杂性问题。下面介绍如何避免过度工程化,构建可维护的系统。

为什么 Go 项目会变成文件迷宫?

从其他语言转到 Go 的开发者往往会带来不适合 Go 习惯的架构做法。根据 Go 开发者调查 2024,最常见问题是由于经验水平不一和非习惯用法,导致难以维护一致的代码标准。例如,创建模仿 Java 的文件夹结构,如 domainapplicationinfrastructure,却不理解核心原则。

卡内基梅隆大学的研究证实:架构问题是技术债的主要来源。在 Go 中,它表现为:

Google AdInline article slot
  • dto 文件夹里满是重复的结构体。
  • 层间用于简单转换的 mappers
  • domain 中的接口依赖基础设施(例如 database/sql)。
  • 用例只是调用仓库,没有任何业务逻辑。

结果:200 个文件,却没有一个描述实际业务规则。Clean Architecture 和 DDD 没有成为解决方案,反而成了问题本身。

DDD:不是文件夹,而是语言和边界

领域驱动设计(Domain-Driven Design)不是一套文件夹,而是建模问题领域的途径。核心概念:

通用语言

开发者和业务利益相关者必须在代码、文档和讨论中使用相同的术语。如果经理说“确认订单”,代码就应该有 Confirm() 方法,而不是 SetStatus()。这能消除困惑,加速新人上手。

Google AdInline article slot

限界上下文

大型系统由多个上下文组成。例如,销售中的“客户”(潜在客户,有销售漏斗)和支持中的“客户”(工单)是不同的模型。限界上下文定义了模型保持一致的清晰边界。在微服务中,一个服务通常对应一个上下文。

实体和值对象

  • Entity(实体)有唯一 ID 并保持身份(如 Order)。它包含业务规则。
  • Value Object(值对象)是不可变的,由其属性定义(如 Money)。字段完全相同的两个对象等价。

Go 中的值对象示例:

type Money struct {
    Amount   int64
    Currency string
}

func (m Money) Add(other Money) (Money, error) {
    if m.Currency != other.Currency {
        return Money{}, errors.New("currency mismatch")
    }
    return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}

聚合

一组被视为单一单元的对象。它有一个根(聚合根),所有访问都通过根进行。例如,Order(根)包含 OrderItem。规则:变更只能通过根进行,确保一致性。

Google AdInline article slot

Clean Architecture:依赖规则是基础

Clean Architecture 的本质是业务逻辑不应依赖实现细节(数据库、框架)。唯一规则:依赖向内指向(Dependency Rule)。

在 Go 中的实现

  • domain:包含业务模型(Entity、Value Object、仓库接口)。除了标准库,不导入外部包。
  • application:协调领域的用例。只依赖 domain。
  • infrastructure:适配器实现(数据库、HTTP 客户端)。通过接口依赖 domain。
  • delivery:入口点(HTTP、gRPC)。依赖 application。

如果 domain 导入 database/sql,架构就错了。正确示例:仓库接口在 domain 中声明,实现放在 infrastructure/postgres 中。

结合 DDD 和 Clean Architecture 的好处

| 方面 | 效果 | 改进指标 |

|---------------|---------------------------------|-------------------|

| 可测试性 | 领域与基础设施隔离 | +40% 覆盖率 |

| 灵活性 | 数小时内更换适配器 | -90% 时间 |

| 可理解性 | 清晰的组件边界 | -70% 上手时间 |

真实项目结构:避免 200 个文件

Go 服务的最佳结构:

internal/
  domain/
    order.go        # 聚合 Order
    order_repo.go   # 仓库接口
  application/
    create_order.go # 用例
  infrastructure/
    postgres/
      order_repo.go # 实现
  delivery/
    http/
      order_handler.go

关键原则:

  • 仓库接口放在 domain,而非 infrastructure
  • 除非真正需要,否则不要创建 dtomappers 文件夹:如果不违反边界,可以跨层复用相同结构体。
  • 业务逻辑只在 domain 中。例如,OrderCancel() 方法检查状态:
func (o *Order) Cancel() error {
    if o.status == StatusShipped {
        return errors.New("cannot cancel shipped order")
    }
    o.status = StatusCancelled
    return nil
}

关键要点

  • 通用语言 是 DDD 的基础。如果代码术语与业务用语不匹配,模型就会出错。
  • 依赖规则 是 Clean Architecture 的核心。违反它会导致意大利面条代码。
  • 实用主义——没有必要就不要创建层。在 Go 中,如果结构体对齐,往往可以跳过 dto
  • 聚合 定义事务边界。为了保持一致性,只通过根进行变更。
  • 测试——领域测试无需基础设施,加速开发。

— Editorial Team

Advertisement 728x90

继续阅读