在基于 Go 的 DDD 分层架构中,包含多个server的项目目录结构应如何组织?
在基于 Go 的 DDD 分层架构中,项目目录结构应如何组织?
在Golang项目中,采用DDD(Domain-Driven Design,领域驱动设计)的分层架构时,没有严格的标准规范,因为DDD更注重设计原则而非固定结构。但根据社区实践和常见实现,通常分为四层:Interface层(或Presentation层,处理外部交互如API)、Application层(协调用例和业务流程)、Domain层(核心领域模型和逻辑)、Infrastructure层(基础设施如数据库、外部服务)。这些层通过接口解耦,确保领域逻辑独立。
以下是一个典型的目录结构示例,基于多个来源的总结(如Medium文章、DEV Community和Talent500博客)。实际项目可根据规模调整,例如小型项目可能将所有层放在单个包下,大型项目则细分子包。结构通常置于internal/
目录下,以防止外部导入。
project-root/
├── cmd/ # 应用入口点,包含main函数
│ └── main.go # 启动服务器或CLI的入口
├── internal/ # 内部包,核心代码
│ ├── application/ # Application层:用例服务,协调领域逻辑(不含业务规则)
│ │ └── services/ # 子目录示例
│ │ └── order_service.go # 示例:订单用例服务
│ ├── domain/ # Domain层:核心业务模型、实体、值对象、领域服务、仓库接口
│ │ ├── model/ # 或直接按领域细分,如order/
│ │ │ └── order.go # 示例:订单实体
│ │ ├── repository/ # 仓库接口(抽象数据访问)
│ │ │ └── order_repository.go
│ │ └── service/ # 领域服务(复杂业务逻辑)
│ │ └── order_domain_service.go
│ ├── infrastructure/ # Infrastructure层:具体实现,如数据库、外部API
│ │ ├── persistence/ # 数据持久化实现
│ │ │ └── order_repository_impl.go # 仓库接口的数据库实现
│ │ ├── datastore/ # 数据存储具体实现(如SQL、NoSQL)
│ │ └── transport/ # 外部通信(如HTTP客户端)
│ │ └── http/ # HTTP相关实现
│ └── interfaces/ # Interface/Presentation层:外部适配器,如HTTP handler、CLI
│ ├── handlers/ # 处理程序
│ │ └── order_handler.go # 示例:订单API handler
│ ├── routes/ # 路由定义
│ │ └── order_routes.go
│ └── middleware/ # 中间件(如日志、认证)
│ └── logging.go
├── pkg/ # 可复用公共包(可选,非DDD核心)
│ └── utils/ # 工具函数
└── go.mod # 模块定义
说明:
- Domain层 是DDD的核心,应保持纯净,只包含业务逻辑,不依赖其他层。仓库接口定义在这里,但实现放在Infrastructure层。
- Application层 处理事务边界和用例(如创建订单流程),调用Domain层的服务和仓库接口。
- Infrastructure层 提供具体技术实现,可替换(如切换数据库)。
- Interfaces层 处理输入输出,如REST API、gRPC或命令行。
- 对于单一领域的小项目,可将所有层合并到一个包(如
tracking/
)中,随着增长再拆分子包。 - 建议使用依赖注入(如Wire)来组装层级,避免循环依赖。
这个结构受Clean Architecture影响,与DDD兼容。如果项目涉及多个领域,可在domain/
下按领域细分(如domain/order/
、domain/user/
)。
如果项目中既需要启动 HTTP Server 和 gRPC Server,同时还包含一个不对外提供服务的 Worker,那么这几类 Server 应当如何划分和归类?
在Golang项目采用DDD分层架构时,对于需要启动多个服务器(如HTTP server、gRPC server)和一个内部worker的场景,目录结构的设计重点在于保持共享的核心逻辑(Domain和Application层)独立,同时通过cmd/
目录支持多个入口点(entry points)。这是一种常见实践,受Clean Architecture和Hexagonal Architecture影响,与DDD兼容,能避免代码重复并便于维护。以下基于社区最佳实践的建议结构和放置方式。
推荐目录结构调整
基于原始DDD四层结构(Interfaces、Application、Domain、Infrastructure),为支持多个服务器,我们在cmd/
下添加子目录,每个子目录对应一个独立的可执行程序(main函数)。这允许每个服务器或worker有自己的启动逻辑,但共享internal/
下的业务代码。结构示例如下:
project-root/
├── cmd/ # 多个入口点,每个子目录是一个可执行程序
│ ├── http-server/ # HTTP服务器入口
│ │ └── main.go # 启动HTTP服务器,注入依赖(如路由、handlers)
│ ├── grpc-server/ # gRPC服务器入口
│ │ └── main.go # 启动gRPC服务器,注册服务实现
│ └── worker/ # 内部worker入口(不对外)
│ └── main.go # 启动worker进程,如消费队列或定时任务
├── internal/ # 核心共享代码
│ ├── application/ # 用例服务,协调业务(如OrderService调用领域逻辑)
│ │ └── services/
│ │ └── order_service.go
│ ├── domain/ # 核心领域模型、实体、仓库接口
│ │ ├── model/
│ │ │ └── order.go
│ │ └── repository/
│ │ └── order_repository.go
│ ├── infrastructure/ # 基础设施实现(如DB、队列、外部服务)
│ │ ├── persistence/ # 数据持久化
│ │ │ └── order_repository_impl.go
│ │ └── queue/ # 如消息队列实现(worker可能依赖)
│ │ └── rabbitmq.go # 示例:worker消费的队列
│ └── interfaces/ # 外部适配器,按协议细分
│ ├── http/ # HTTP相关:handlers、routes、中间件
│ │ ├── handlers/
│ │ │ └── order_handler.go
│ │ └── routes/
│ │ └── order_routes.go
│ └── grpc/ # gRPC相关:proto文件、服务实现
│ ├── proto/ # .proto文件和生成的代码
│ │ └── order.proto
│ └── server/ # gRPC服务实现
│ └── order_grpc.go # 实现gRPC接口,调用Application服务
├── pkg/ # 可复用工具(可选)
│ └── utils/
└── go.mod
放置说明
-
HTTP Server:
- 放置在
cmd/http-server/main.go
中。这里是启动HTTP服务器的入口,例如使用Gin或net/http库,注册interfaces/http/
下的routes和handlers。 - 为什么在这里?
cmd/
适合应用入口,避免污染核心层。main函数中通过依赖注入(DI,如使用Wire或手动)组装Application服务、仓库等,然后启动服务器(如http.ListenAndServe()
)。 - 示例代码片段(main.go):
package mainimport ("yourproject/internal/application/services""yourproject/internal/interfaces/http/handlers""yourproject/internal/interfaces/http/routes"// ... 其他导入 )func main() {// 初始化依赖(如DB连接、仓库)orderService := services.NewOrderService(/* 注入仓库 */)router := routes.SetupRouter(orderService) // 设置路由http.ListenAndServe(":8080", router) }
- Handlers和routes放在
interfaces/http/
下,确保与领域逻辑解耦。
- 放置在
-
gRPC Server:
- 放置在
cmd/grpc-server/main.go
中。这里启动gRPC服务器,例如使用grpc-go库,注册interfaces/grpc/server/
下的服务实现。 - 为什么在这里?类似于HTTP,gRPC作为另一种外部接口,但独立入口允许单独部署或 scaling。proto文件和生成的代码放在
interfaces/grpc/proto/
,服务实现调用Application层用例。 - 示例代码片段(main.go):
package mainimport ("google.golang.org/grpc""yourproject/internal/application/services""yourproject/internal/interfaces/grpc/server"// ... proto导入 )func main() {s := grpc.NewServer()orderService := services.NewOrderService(/* 注入 */)pb.RegisterOrderServer(s, server.NewOrderGRPCServer(orderService))s.Serve(/* 监听端口 */) }
- 这保持gRPC独立于HTTP,但共享Domain和Application逻辑。
- 放置在
-
Worker(内部不对外服务):
- 放置在
cmd/worker/main.go
中。这里启动后台进程,例如使用goroutines处理定时任务、消费消息队列(如RabbitMQ或Kafka),或运行批处理。 - 为什么在这里?Worker不是外部接口,不需要handlers,而是直接调用Application服务或Infrastructure实现。放在
cmd/
下作为独立可执行,便于单独运行(如在Kubernetes中作为sidecar或job)。 - 示例代码片段(main.go):
package mainimport ("yourproject/internal/application/services""yourproject/internal/infrastructure/queue"// ... )func main() {// 初始化依赖orderService := services.NewOrderService(/* 注入 */)consumer := queue.NewRabbitMQConsumer(orderService)consumer.Start() // 无限循环消费队列 }
- 相关基础设施(如队列客户端)放在
infrastructure/queue/
下。
- 放置在
额外建议
- 依赖注入:使用Google Wire或fx来组装层级依赖,确保main函数简洁。
- 配置和日志:在每个main中处理特定配置(如端口、队列连接),但共享通用配置(如通过环境变量或config包)。
- 部署考虑:每个cmd子目录编译成独立二进制,便于微服务化。如果项目规模小,可先合并到一个main中用flag切换模式,但多入口更灵活。
- 为什么不放在其他层:DDD强调Domain独立,Interfaces只处理适配,Application协调用例。启动逻辑是"应用"级别,故放cmd/。
- 如果项目涉及多个领域,可进一步在domain/下按领域细分(如domain/order/、domain/user/),服务器入口相应调整。
这个结构在实际项目中证明有效,便于测试和扩展。如果你的项目有特定框架(如Echo for HTTP、grpc-go for gRPC),可据此调整。