在 Go 中用 DDD 风格组织代码:实践、目录与命名规范(可落地)
# 摘要
本文给出一个落地可执行的 Go + DDD 代码组织方案。包含:分层含义、推荐目录树、包命名与文件命名规范、典型代码片段(领域实体、仓储接口、应用服务、适配器/实现)、防坑注意项与代码风格建议。侧重 最小侵入、依赖倒置、包循环避免 与 Go 风格(gofmt、小写包名、单一职责、内建 internal 与 pkg 约束)。
# 1. 基本原则(与 Go 的契合点)
- 领域优先、基础设施靠后:领域模型(
domain)不依赖任何数据库、Web、框架或第三方库。 - 依赖倒置:高层(application / adapters)依赖领域接口(domain interfaces),具体实现注入到运行时。
- 包的最小化与单一职责:一个包只聚焦一组职责(例如
order聚合、user聚合),避免出现“god package”。 - 使用
internal隔离实现细节:对外暴露只放在pkg或根导出包,其他实现放internal。 - 避免循环依赖:通过接口与构造函数解耦,或把共享类型放到
pkg(谨慎)或shared(尽量少用)。
# 2. 推荐目录结构(示例)
myapp/
├── cmd/
│ └── myapp/ # 程序入口(main.go)——可多个 cmd
│ └── main.go
├── configs/ # 配置文件、模板
├── internal/
│ ├── domain/ # 领域层(只含接口、实体、领域服务、事件)
│ │ ├── user/
│ │ │ ├── entity.go
│ │ │ ├── value_objects.go
│ │ │ └── repository.go # 仓储接口
│ │ └── order/
│ │ └── ...
│ ├── application/ # 应用层(用例/事务协调、DTO、服务)
│ │ └── usercase/
│ │ ├── service.go
│ │ └── dto.go
│ ├── adapter/ # 适配器(输入/输出)实现
│ │ ├── http/
│ │ │ └── user_handler.go
│ │ └── grpc/
│ └── infra/ # 基础设施(DB、缓存、邮件、外部系统)
│ ├── db/
│ │ └── postgres_user_repo.go
│ └── logger/
├── api/ # OpenAPI/Protos/接口定义
├── pkg/ # 可以被外部项目复用的包(小心使用)
├── scripts/
├── deployments/
└── go.mod
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
说明:
- 把业务代码放
internal,确保不会被外部 import(增强封装)。 domain只包含领域概念与仓储接口,不包含任何 ORM 或 HTTP 相关内容。adapter或infra放置实现(仓储实现、HTTP handler、消息总线消费者等)。
# 3. 包命名与文件命名规范
- 包名:短小、单数、小写(例如
user,order,payment),不要下划线或驼峰。 - 避免使用
domain下的通用包名(如models、common)—这些通常招致职责不清。 - 文件名:按职责细分(
entity.go,repository.go,service.go,handler_http.go),避免单文件过大。 - 类型命名:导出类型使用大写(
User,Order);非导出使用小写。 - 接口命名:接口名建议用行为后缀(
UserRepository,PaymentGateway),而不是IUser风格。 - 变量/函数命名:驼峰小写,导出函数首字母大写。
# 4. 领域层(internal/domain/...)
特点:纯净、无依赖实现细节。
示例:internal/domain/user/entity.go
package user
import "time"
// User 是聚合根
type User struct {
ID string
Email string
Password string // 哈希
CreatedAt time.Time
}
func NewUser(id, email, passwordHash string) *User {
return &User{ID: id, Email: email, Password: passwordHash, CreatedAt: time.Now()}
}
func (u *User) ChangeEmail(new string) {
// 领域验证/不变式在此处实现
u.Email = new
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例:internal/domain/user/repository.go
package user
// UserRepository 定义了持久化契约(接口)
type UserRepository interface {
Save(u *User) error
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
}
2
3
4
5
6
7
8
说明:领域层定义契约(接口),但不包含实现。实现应在 internal/infra 或 internal/adapter/persistence。
# 5. 应用层(internal/application/...)
职责:编排应用用例、组织事务、调用领域接口、转换 DTO。
示例:internal/application/usercase/service.go
package usercase
import (
"errors"
"myapp/internal/domain/user"
)
type UserService struct {
repo user.UserRepository
}
func NewUserService(r user.UserRepository) *UserService { return &UserService{repo: r} }
func (s *UserService) Register(email, passwordHash string) (*user.User, error) {
// 1. 领域规则校验(可调用领域服务)
if existing, _ := s.repo.FindByEmail(email); existing != nil {
return nil, errors.New("email exists")
}
u := user.NewUser(generateID(), email, passwordHash)
if err := s.repo.Save(u); err != nil { return nil, err }
return u, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意:应用层可以处理事务边界(例如开始/提交 DB 事务),但应保持尽可能薄。
# 6. 适配器/基础设施层(internal/adapter, internal/infra)
职责:把领域接口连接到具体技术实现(ORM、HTTP、消息队列)。
示例:internal/infra/db/postgres_user_repo.go
package db
import (
"database/sql"
"myapp/internal/domain/user"
)
type PostgresUserRepo struct { db *sql.DB }
func NewPostgresUserRepo(db *sql.DB) *PostgresUserRepo { return &PostgresUserRepo{db: db} }
func (r *PostgresUserRepo) Save(u *user.User) error {
// SQL 操作
return nil
}
func (r *PostgresUserRepo) FindByID(id string) (*user.User, error) { return nil, nil }
func (r *PostgresUserRepo) FindByEmail(email string) (*user.User, error) { return nil, nil }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在 cmd/myapp/main.go 中构建依赖关系并注入:
func main() {
db := mustOpenDB()
userRepo := db.NewPostgresUserRepo(dbConn)
userSvc := usercase.NewUserService(userRepo)
// wire up http handlers with userSvc
}
2
3
4
5
6
# 7. 关于包名 user.User 的讨论
你可能注意到:在 Go 里 user.User(包名 + 类型名)经常出现。这是正常且符合惯例的。它能清晰表达:类型 User 属于 user 包。但要注意:
- 若觉得冗余,可以在导入时起别名:
u "myapp/internal/domain/user",使用u.User。 - 不推荐把包也命名为
models、entities之类通用名,这会降低可读性。 - 建议按 聚合(aggregate)建包:
domain/user、domain/order,更契合 DDD。
# 8. 事务与一致性边界
- 短事务:在应用层开启并提交(例如在 service 方法内),把事务对象通过仓储实现传递。
- 跨聚合事务:优先用最终一致性(事件/消息)而不是分布式事务(两阶段),除非必须。
样例:在 application 层开始事务:
// pseudo
func (s *Service) DoSomething(...) error {
tx := db.Begin()
defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }()
// repo 使用 tx 实现
}
2
3
4
5
6
# 9. 测试建议
- 领域单测:只针对
internal/domain/*的行为和不变式编写单元测试(无需 DB)。 - 集成测试:把
infra的实现替换为 test doubles 或使用测试 DB(docker-compose/testcontainers)。 - 端到端测试:在 CI 中用真实依赖(容器化 DB / MQ)跑 e2e 测试。
# 10. 常见反模式与注意事项
- 把 ORM 代码放进 domain:违反依赖倒置。领域应该只关心接口。
- 大而全的
models包:职责模糊,容易形成循环依赖。 - 全局状态/单例数据库变量:不利于测试与并发。
- 大量使用
pkg/shared:导致隐性耦合,优先用明确的接口注入。 - 忽略错误包装:在边界处用
errors.Wrap或%w链接错误上下文,便于排查。
# 11. 代码风格 & 工具链建议
gofmt/goimports/golangci-lint(开启govet,staticcheck)。- 采用
go test ./...在 CI 中持续运行。使用-race在集成测试中检查竞态。 - 依赖管理用
go mod,避免把vendor弹性化管理滥用。
# 12. 实战示例(完整包关系小结)
internal/domain/user:实体、值对象、仓储接口、领域服务。internal/application/usercase:应用服务、DTO、用例编排。internal/infra/db:Postgres/MySQL 的仓储实现。internal/adapter/http:HTTP 层(处理 request -> DTO -> 调用 application service),返回标准化错误/响应。
这样可以保证:领域不依赖 infra;infra 依赖 domain;application 把 domain 与 infra 组装起来。
# 13. 最后一张清单(落地前检查)
- [ ] 领域层:无任何第三方依赖。
- [ ] 仓储接口放领域,具体实现放 infra/adapter。
- [ ] 使用
internal隔离实现,暴露可复用组件放pkg。 - [ ] 包名小写、单数、职责单一。
- [ ] 主函数(
cmd/)负责依赖注入与启动。 - [ ] CI 覆盖
gofmt,golangci-lint,go test。
# 结语
把 DDD 的思路带入 Go 项目不是为了机械搬运概念,而是把“边界明确、职责单一、可测试、易演进”的工程实践落地。用 internal、短小清晰的包名、接口注入与慎用共享包,就能既保留 Go 的简洁,又实现领域驱动的长期可维护性。