[Go 模板] DDD 四层架构骨架:依赖倒置 + Value Object + 防腐层示范
- 内容介绍
- 文章标签
- 相关推荐
本帖使用社区公益推广,符合推广要求。我申明并遵循社区要求的以下内容:
- 我的项目是免费使用的,无收费(变相收费、赞助)部分: 是
- 我的帖子已经打上 公益推广 标签: 是
- 我的项目属于个人项目,与公司或商业机构无关: 是
- 我的项目不存在QQ、TG等群组引流: 是
- 我的项目不存在非运营必要的网站引流: 是
- 我的项目不存在为他人推广、AFF: 是
- 我的项目无关联的商业项目: 是
- 我的站点存在登录,并已接入 LINUX DO Connect: 是
- 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是
- 以上选择我承诺是永久有效的,接受社区和佬友监督: 是
以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出
背景:
以前在某游戏公司做后端开发,当时采用的代码架构就是MVC,但随着业务发展,服务端代码膨胀到了几十万的屎山巨物,做需求时常面对牵一发而动全身的尴尬境地。常出现开发没发现,测试没测出,上线导致线上火葬场。
问题不在于代码量大,而在于没有边界。业务逻辑、数据库操作、第三方调用全部混在一起,任何一层的变化都会向外扩散。
后来阅读了一些中大型 Go 项目的代码,开始接触 DDD 的分层思路,逐渐理解了边界的意义。这个模板就是我的一次整理,把 DDD的核心分层结构用 Go 落地,分享给各位佬友。
image772×567 92.7 KB
https://github.com/Martindeeepdark/go-template
整体架构:依赖只有一个方向
在讲具体实现之前,先把"地图"摆出来。整个项目分为四层:
依赖方向只有一个:外 → 内
handler → application → domain ← infrastructure
注意 infrastructure 那个箭头是反的——它不是 domain 依赖 infrastructure,而是 infrastructure 去实现 domain
定义的接口。这就是依赖倒置的核心:内层定规则,外层做实现,内层永远不知道外层的存在。
这条规则贯穿整个项目,下面的每一个设计决策都是它的具体体现。
1. 依赖倒置:domain 层不感知数据库
传统 MVC 里,service 直接调 db.Query(…),换 ORM 或者换数据库就是噩梦。
这个模板的做法:domain 层定义接口,infrastructure 层实现它。
// domain/user/repository/user.go
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*entity.User, error)
Save(ctx context.Context, user *entity.User) error
}
// infrastructure/persistence/user_repo.go
type userRepo struct{ db *gorm.DB }
func (r *userRepo) FindByID(ctx context.Context, id int64) (*entity.User, error) {
// GORM 实现细节在这里
}
domain 层永远不 import infrastructure,依赖关系单向清晰。想换 ORM?只改 infrastructure,domain 和 application 层零感知。
2. 值对象: 字段,但不是裸字符串
MVC 屎山里有一种很常见的气味:校验逻辑零散地分布在各个 service 里,同样的规则重复出现,业务一变,要全局搜索才知道改哪里。
DDD 的做法是把字段包成有行为的类型,校验逻辑收敛到实体内部,外部拿到这个类型,就意味着它一定是合法的。
type User struct {
ID int64
Username string
Nickname string
Email Email
Phone Phone
Avatar string
Status int32
HashedPassword string
CreatedAt int64
UpdatedAt int64
}
type Email string
func (e Email) IsValid() bool {
if len(e) == 0 {
return false
}
for i := 0; i < len(e); i++ {
if e[i] == '@' {
return true
}
}
return false
}
type Phone string
func (p Phone) IsValid() bool {
return len(p) == 11
}
func (p Phone) String() string {
return string(p)
}
每一个值对象的校验逻辑都只在一个地方,业务规则变了只改这里,不再追着全项目改散落的 if 判断。
3.防腐层:隔离第三方变化
第三方 SDK 的接口随时可能改,直接在 service 里调用就是把外部变化的风险引入核心逻辑。
统一把第三方调用收在 infrastructure/ 下,domain 层通过接口调用
外部换了 SDK,改的只是 infrastructure里的实现,业务逻辑不动。
4.依赖注入
有了分层和接口,依赖关系怎么组装?这个模板把所有组装逻辑集中在 cmd/userservice/command.go 这一个地方:
func start(configPath string) error {
// 加载配置
v, _ := configs.Load(configPath)
// 初始化 DB
db, _ := gorm.Open(mysql.Open(v.GetString("data.database.source")), &gorm.Config{})
// 依赖链从外到内,一眼看清
repo := infrastructure.NewUserRepository(db)
userSvc := service.NewUserService(repo)
appSvc := application.NewAppService(userSvc)
userHandler := handler.NewUserHandler(appSvc)
srv := server.NewHTTPServer(v.GetString("server.http.addr"))
srv.Register(userHandler)
return srv.Start()
}
四行代码,整条依赖链一目了然。各层自己不负责创建依赖,测试时替换某一层的实现也只需要改这里。
最后
这个模板只解决一件事:告诉你代码应该放在哪里,依赖应该怎么流动。
日志用 slog 还是 zap、缓存怎么设计、中间件怎么写——这些是业务决策,模板不替你做。
DDD 不是银弹,小项目用它是过度设计。但当业务逻辑开始变复杂,当你发现一个字段改动在向每一层扩散,当你想替换某个基础设施组件却发现牵一发动全身——那时候分层边界的价值才真正体现出来。
希望这个骨架能帮各位佬友少踩一些坑,有问题欢迎讨论。
网友解答:--【壹】--: ZhuChL:
DDD理论并不算架构。理论理解很容易,开发时候其实也蛮痛苦的,需要对业务充分理解,就是对领域充分理解,需要稳固的业务。MVC堆砌很容易,数据库设计也容易,但是治理数据库和理解屎山就难了。
F-Martin:逐渐理解了边界的意义
这部分实操太难了,所以期待佬的文章,这部分也是不同专家不同意见。
我的感觉是反过来的
实体和领域的区分是比较简单的,跟着业务聊熟了自然就出来了。真正的难点是聚合根和限界上下文
聚合根难在:它不只是根对象,它是一致性边界。你要决定哪些对象的状态变更必须在同一个事物里面保证一致,哪些保证最终一致性。这个判断直接影响并发设计和性能 聚合太大,并发冲突多;太小,业务规则散落各处。
限界上下文难在:他是语言的边界,订单含义在交易上下文和物流上下文是截然不同的,完全是两个对象,强行耦合在一起才是屎山的源头。
这里也是DDD和MVC的分水岭,MVC不强制你思考这些问题,所以Service越写越大,到最后谁也不敢动
--【贰】--:
个人项目无脑MVC就行 一个人维护的话 不用管理太多多人协作的问题。DDD不是银弹,简单场景下引入DDD只会增加系统的复杂度。
但是我觉得可以多去看看go编码规范和一些哲学实践。例如使用方去定义interface,这里和java是反直觉的,但是是我认为能够有效帮助你解决改一处波及一片的问题的。
而且domain层是具备独立id的实体,它是有自己的业务规则和方法的,而不是因为不适合放在service和repo才放到这里的。有兴趣可以看看我模版里面的代码样例
--【叁】--:
这个落地确实是对团队的考验,但我认为的DDD不是为了提高上限而使用,是为了保住团队的下限。
前段时间看了守望先锋的ECS布道文档,也觉得收获颇丰
--【肆】--:
DDD里面有一个领域专家的角色,所谓的领域专家角色大多时候是产品和开发承担了,这两者有时候是对立的,业务拎不清。如果专家熟练掌握业务,即使新项目,也很自然、很容易框定变更;如果遇到走一看一步的项目和假专家,DDD实操比较困难。 以前想互联网有时候就是不适合DDD,互联网会追新的项目增长点和技术热点,DDD也是KPI引导出来
--【伍】--:
还是看情况 如果业务快速迭代的话 业务还没有沉淀下来就开始DDD会是非常痛苦的。DDD开场基本就是几千代码出去了 MVC的规模小多了 非常适合迭代的节奏
--【陆】--:
佬可以看一下kratos,他比你多了个api层用来解析参数,但是把service和domain缩成了biz
--【柒】--:
佬友们,记得看看俺的github,给点star给点建议,俺才能迭代出更好的骨架呀
--【捌】--:
domain和值对象很像我们现在用的kratos 这套,明天仔细看看细节
--【玖】--:
kratos和go-zero我都深度看过。
kratos的api以protof此类IDL接口文档来驱动,同时对外开放了gRPC和rest接口。我在模版里面留了api的路径 但是api的具体实现 我觉得应该是使用者自己来决定,不能只限制在.api 或者.protof这种文件里面。
kratos我觉得设计不太好的一点就是 biz里面是一个大杂烩的感觉
image603×484 24.1 KB
biz太宽泛,导致极其容易写出MVC味的DDD,如果是想要写MVC的快速迭代的话,我觉得kratos这套也不灵活。
--【拾】--:
感谢分享,最近也在研究GO的相关架构
请问如果是个人开发的小项目,有什么比较好的方案吗?
一开始我寻思着随便写写,代码多了后改了一处波及一片,后面和AI讨论了下琢磨出一个DDD+清洁架构融合版。
大概是:
http -> service -> (domian) -> repository <- model
http负责路由啥的
service负责编排
domian写一些不适合放在service和repository的代码
repository和数据库交互
瞎琢磨的,没有真实案例沉淀,写的时候感觉还是怪怪的,楼主有什么好想法吗?
--【拾壹】--:
ddd不仅是分层隔离,还要求domain和domain之间不能互相依赖,这就要求很深的业务理解。大概率需求一多就开始胡搞了…
--【拾贰】--:
感谢大佬
--【拾叁】--:
感谢提点。之前看很多评论和文章,很多人都摈弃在go里用mvc,说是写的跟java一样,所以下意识排除MVC了。仔细想想目前对我而言MVC确实是最好的选择
--【拾肆】--:
聚合根设计确实是难点
我觉得先要有稳固的业务再有 DDD 这件事是比较反直觉的,其实设计良好的 DDD 反而能框定变更的影响面
--【拾伍】--:
DDD理论并不算架构。理论理解很容易,开发时候其实也蛮痛苦的,需要对业务充分理解,就是对领域充分理解,需要稳固的业务。MVC堆砌很容易,数据库设计也容易,但是治理数据库和理解屎山就难了。
F-Martin:逐渐理解了边界的意义
这部分实操太难了,所以期待佬的文章,这部分也是不同专家不同意见。
--【拾陆】--:
DDD这个东西,千人千面,概念都懂,但是落地的时候怎么写,怎么实现千奇百怪,很多时候如果硬套DDD的思想,也会让代码变得不友好,目前学习下来感觉大概念包括分层思想、领域模型、限界上下文这些确实对整个工程是友好的,但是真正的实现细节里很难完全遵循DDD的这套逻辑。
--【拾柒】--:
确实,感觉biz的设计有些莫名其妙 我实习的时候api分成了proto的/api,还有业务逻辑中的/internal/api
本帖使用社区公益推广,符合推广要求。我申明并遵循社区要求的以下内容:
- 我的项目是免费使用的,无收费(变相收费、赞助)部分: 是
- 我的帖子已经打上 公益推广 标签: 是
- 我的项目属于个人项目,与公司或商业机构无关: 是
- 我的项目不存在QQ、TG等群组引流: 是
- 我的项目不存在非运营必要的网站引流: 是
- 我的项目不存在为他人推广、AFF: 是
- 我的项目无关联的商业项目: 是
- 我的站点存在登录,并已接入 LINUX DO Connect: 是
- 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是
- 以上选择我承诺是永久有效的,接受社区和佬友监督: 是
以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出
背景:
以前在某游戏公司做后端开发,当时采用的代码架构就是MVC,但随着业务发展,服务端代码膨胀到了几十万的屎山巨物,做需求时常面对牵一发而动全身的尴尬境地。常出现开发没发现,测试没测出,上线导致线上火葬场。
问题不在于代码量大,而在于没有边界。业务逻辑、数据库操作、第三方调用全部混在一起,任何一层的变化都会向外扩散。
后来阅读了一些中大型 Go 项目的代码,开始接触 DDD 的分层思路,逐渐理解了边界的意义。这个模板就是我的一次整理,把 DDD的核心分层结构用 Go 落地,分享给各位佬友。
image772×567 92.7 KB
https://github.com/Martindeeepdark/go-template
整体架构:依赖只有一个方向
在讲具体实现之前,先把"地图"摆出来。整个项目分为四层:
依赖方向只有一个:外 → 内
handler → application → domain ← infrastructure
注意 infrastructure 那个箭头是反的——它不是 domain 依赖 infrastructure,而是 infrastructure 去实现 domain
定义的接口。这就是依赖倒置的核心:内层定规则,外层做实现,内层永远不知道外层的存在。
这条规则贯穿整个项目,下面的每一个设计决策都是它的具体体现。
1. 依赖倒置:domain 层不感知数据库
传统 MVC 里,service 直接调 db.Query(…),换 ORM 或者换数据库就是噩梦。
这个模板的做法:domain 层定义接口,infrastructure 层实现它。
// domain/user/repository/user.go
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*entity.User, error)
Save(ctx context.Context, user *entity.User) error
}
// infrastructure/persistence/user_repo.go
type userRepo struct{ db *gorm.DB }
func (r *userRepo) FindByID(ctx context.Context, id int64) (*entity.User, error) {
// GORM 实现细节在这里
}
domain 层永远不 import infrastructure,依赖关系单向清晰。想换 ORM?只改 infrastructure,domain 和 application 层零感知。
2. 值对象: 字段,但不是裸字符串
MVC 屎山里有一种很常见的气味:校验逻辑零散地分布在各个 service 里,同样的规则重复出现,业务一变,要全局搜索才知道改哪里。
DDD 的做法是把字段包成有行为的类型,校验逻辑收敛到实体内部,外部拿到这个类型,就意味着它一定是合法的。
type User struct {
ID int64
Username string
Nickname string
Email Email
Phone Phone
Avatar string
Status int32
HashedPassword string
CreatedAt int64
UpdatedAt int64
}
type Email string
func (e Email) IsValid() bool {
if len(e) == 0 {
return false
}
for i := 0; i < len(e); i++ {
if e[i] == '@' {
return true
}
}
return false
}
type Phone string
func (p Phone) IsValid() bool {
return len(p) == 11
}
func (p Phone) String() string {
return string(p)
}
每一个值对象的校验逻辑都只在一个地方,业务规则变了只改这里,不再追着全项目改散落的 if 判断。
3.防腐层:隔离第三方变化
第三方 SDK 的接口随时可能改,直接在 service 里调用就是把外部变化的风险引入核心逻辑。
统一把第三方调用收在 infrastructure/ 下,domain 层通过接口调用
外部换了 SDK,改的只是 infrastructure里的实现,业务逻辑不动。
4.依赖注入
有了分层和接口,依赖关系怎么组装?这个模板把所有组装逻辑集中在 cmd/userservice/command.go 这一个地方:
func start(configPath string) error {
// 加载配置
v, _ := configs.Load(configPath)
// 初始化 DB
db, _ := gorm.Open(mysql.Open(v.GetString("data.database.source")), &gorm.Config{})
// 依赖链从外到内,一眼看清
repo := infrastructure.NewUserRepository(db)
userSvc := service.NewUserService(repo)
appSvc := application.NewAppService(userSvc)
userHandler := handler.NewUserHandler(appSvc)
srv := server.NewHTTPServer(v.GetString("server.http.addr"))
srv.Register(userHandler)
return srv.Start()
}
四行代码,整条依赖链一目了然。各层自己不负责创建依赖,测试时替换某一层的实现也只需要改这里。
最后
这个模板只解决一件事:告诉你代码应该放在哪里,依赖应该怎么流动。
日志用 slog 还是 zap、缓存怎么设计、中间件怎么写——这些是业务决策,模板不替你做。
DDD 不是银弹,小项目用它是过度设计。但当业务逻辑开始变复杂,当你发现一个字段改动在向每一层扩散,当你想替换某个基础设施组件却发现牵一发动全身——那时候分层边界的价值才真正体现出来。
希望这个骨架能帮各位佬友少踩一些坑,有问题欢迎讨论。
网友解答:--【壹】--: ZhuChL:
DDD理论并不算架构。理论理解很容易,开发时候其实也蛮痛苦的,需要对业务充分理解,就是对领域充分理解,需要稳固的业务。MVC堆砌很容易,数据库设计也容易,但是治理数据库和理解屎山就难了。
F-Martin:逐渐理解了边界的意义
这部分实操太难了,所以期待佬的文章,这部分也是不同专家不同意见。
我的感觉是反过来的
实体和领域的区分是比较简单的,跟着业务聊熟了自然就出来了。真正的难点是聚合根和限界上下文
聚合根难在:它不只是根对象,它是一致性边界。你要决定哪些对象的状态变更必须在同一个事物里面保证一致,哪些保证最终一致性。这个判断直接影响并发设计和性能 聚合太大,并发冲突多;太小,业务规则散落各处。
限界上下文难在:他是语言的边界,订单含义在交易上下文和物流上下文是截然不同的,完全是两个对象,强行耦合在一起才是屎山的源头。
这里也是DDD和MVC的分水岭,MVC不强制你思考这些问题,所以Service越写越大,到最后谁也不敢动
--【贰】--:
个人项目无脑MVC就行 一个人维护的话 不用管理太多多人协作的问题。DDD不是银弹,简单场景下引入DDD只会增加系统的复杂度。
但是我觉得可以多去看看go编码规范和一些哲学实践。例如使用方去定义interface,这里和java是反直觉的,但是是我认为能够有效帮助你解决改一处波及一片的问题的。
而且domain层是具备独立id的实体,它是有自己的业务规则和方法的,而不是因为不适合放在service和repo才放到这里的。有兴趣可以看看我模版里面的代码样例
--【叁】--:
这个落地确实是对团队的考验,但我认为的DDD不是为了提高上限而使用,是为了保住团队的下限。
前段时间看了守望先锋的ECS布道文档,也觉得收获颇丰
--【肆】--:
DDD里面有一个领域专家的角色,所谓的领域专家角色大多时候是产品和开发承担了,这两者有时候是对立的,业务拎不清。如果专家熟练掌握业务,即使新项目,也很自然、很容易框定变更;如果遇到走一看一步的项目和假专家,DDD实操比较困难。 以前想互联网有时候就是不适合DDD,互联网会追新的项目增长点和技术热点,DDD也是KPI引导出来
--【伍】--:
还是看情况 如果业务快速迭代的话 业务还没有沉淀下来就开始DDD会是非常痛苦的。DDD开场基本就是几千代码出去了 MVC的规模小多了 非常适合迭代的节奏
--【陆】--:
佬可以看一下kratos,他比你多了个api层用来解析参数,但是把service和domain缩成了biz
--【柒】--:
佬友们,记得看看俺的github,给点star给点建议,俺才能迭代出更好的骨架呀
--【捌】--:
domain和值对象很像我们现在用的kratos 这套,明天仔细看看细节
--【玖】--:
kratos和go-zero我都深度看过。
kratos的api以protof此类IDL接口文档来驱动,同时对外开放了gRPC和rest接口。我在模版里面留了api的路径 但是api的具体实现 我觉得应该是使用者自己来决定,不能只限制在.api 或者.protof这种文件里面。
kratos我觉得设计不太好的一点就是 biz里面是一个大杂烩的感觉
image603×484 24.1 KB
biz太宽泛,导致极其容易写出MVC味的DDD,如果是想要写MVC的快速迭代的话,我觉得kratos这套也不灵活。
--【拾】--:
感谢分享,最近也在研究GO的相关架构
请问如果是个人开发的小项目,有什么比较好的方案吗?
一开始我寻思着随便写写,代码多了后改了一处波及一片,后面和AI讨论了下琢磨出一个DDD+清洁架构融合版。
大概是:
http -> service -> (domian) -> repository <- model
http负责路由啥的
service负责编排
domian写一些不适合放在service和repository的代码
repository和数据库交互
瞎琢磨的,没有真实案例沉淀,写的时候感觉还是怪怪的,楼主有什么好想法吗?
--【拾壹】--:
ddd不仅是分层隔离,还要求domain和domain之间不能互相依赖,这就要求很深的业务理解。大概率需求一多就开始胡搞了…
--【拾贰】--:
感谢大佬
--【拾叁】--:
感谢提点。之前看很多评论和文章,很多人都摈弃在go里用mvc,说是写的跟java一样,所以下意识排除MVC了。仔细想想目前对我而言MVC确实是最好的选择
--【拾肆】--:
聚合根设计确实是难点
我觉得先要有稳固的业务再有 DDD 这件事是比较反直觉的,其实设计良好的 DDD 反而能框定变更的影响面
--【拾伍】--:
DDD理论并不算架构。理论理解很容易,开发时候其实也蛮痛苦的,需要对业务充分理解,就是对领域充分理解,需要稳固的业务。MVC堆砌很容易,数据库设计也容易,但是治理数据库和理解屎山就难了。
F-Martin:逐渐理解了边界的意义
这部分实操太难了,所以期待佬的文章,这部分也是不同专家不同意见。
--【拾陆】--:
DDD这个东西,千人千面,概念都懂,但是落地的时候怎么写,怎么实现千奇百怪,很多时候如果硬套DDD的思想,也会让代码变得不友好,目前学习下来感觉大概念包括分层思想、领域模型、限界上下文这些确实对整个工程是友好的,但是真正的实现细节里很难完全遵循DDD的这套逻辑。
--【拾柒】--:
确实,感觉biz的设计有些莫名其妙 我实习的时候api分成了proto的/api,还有业务逻辑中的/internal/api

![[Go 模板] DDD 四层架构骨架:依赖倒置 + Value Object + 防腐层示范](/imgrand/LOCis4ak.webp)