[Go 模板] DDD 四层架构骨架:依赖倒置 + Value Object + 防腐层示范

2026-04-11 13:291阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

本帖使用社区公益推广,符合推广要求。我申明并遵循社区要求的以下内容:

  • 我的项目是免费使用的,无收费(变相收费、赞助)部分:
  • 我的帖子已经打上 公益推广 标签:
  • 我的项目属于个人项目,与公司或商业机构无关:
  • 我的项目不存在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