Golang整洁架构实践
👉 腾小云导读
为了降低系统组件之间的耦合、提升系统的可维护性,一个好的代码框架显得尤为重要。本文将为大家介绍众所周知的三种代码框架,并从三种框架引申出COLA 架构以及作者基于 COLA 架构设计的 Go 语言项目脚手架实践方案。希望能给广大开发爱好者带来帮助和启发!👉 看目录,点收藏
1.为什么要有代码架构
2.好的代码架构是如何构建的
2.1 整洁架构
2.2 洋葱架构
2.3 六边形架构
2.4 COLA架构
3.推荐一种 Go 代码架构实践
4.总结
*本文提及的架构主要指项目组织的“代码架构”,注意与微服务架构等名词中的服务架构进行区分。
01
历史悠久的项目大都会有很多开发人员参与“贡献”,在没有好的指导规则约束的情况下,大抵会变成一团乱麻。剪不断,理还乱,也没有开发勇士愿意去剪去理。被迫接手的开发勇士如果想要增加一个小需求,可能需要花10倍的时间去理顺业务逻辑,再花 10 倍的时间去补充测试代码,实在是低效又痛苦。
这是一个普遍的痛点问题,有无数开发者尝试过去解决它。这么多年发展下来,业界自然也诞生了很多软件架构。大家耳熟能详的就有六边形架构(Hexagonal Architecture),洋葱架构(Onion Architecture),整洁架构(Clean Architecture)等。
这些架构在细节上有所差异,但是核心目标是一致的:致力于实现软件系统的关注点分离(separation of concerns)。
关注点分离之后的软件系统都具备如下特征:
不依赖特定 UI。UI 可以任意替换,不会影响系统中其他组件。从 Web UI 变成桌面 UI,甚至变成控制台 UI 都无所谓,业务逻辑不会被影响。
不依赖特定框架。以 JavaScript 生态举例,不管是使用 web 框架 koa、express,还是使用桌面应用框架 electron,还是控制台框架 commander,业务逻辑都不会被影响,被影响的只会是框架接入的那一层。
不依赖特定外部组件。系统可以任意使用 MySQL、MongoDB或 Neo4j 作为数据库,任意使用 Redis、Memcached或 etcd 作为键值存储等。业务逻辑不会因为这些外部组件的替换而变化。
容易测试。核心业务逻辑可以在不需要 UI、不需要数据库、不需要 Web 服务器等一切外界组件的情况下被测试。这种纯粹的代码逻辑意味着清晰容易的测试。
软件系统有了这些特征后,易于测试,更易于维护、更新,大大减轻了软件开发人员的心理负担。所以,好的代码架构值得推崇。
02
前文所述的三个架构在理念上是近似的,从下文图 1 到图 3 三幅架构图中也能看出相似的圈层结构。图中可以看到,越往外层越具体,越往内层越抽象。这也意味着,越往外越有可能发生变化,包括但不限于框架升级、中间件变更、适配新终端等等。
2.1 整洁架构
图 1 The Clean Architecture, Robert C. Martin
type Comment struct {...}
核心层的外层是应用业务层
type BlogManager interface {
CreateBlog(...) ...
LeaveComment(...) ...
}
应用业务层的外层是接口适配层
type BlogDTO struct { // Data Transfer Object
Content string `json:"..."`
}
// DTO 与 model.Blog 的转化在此层完成
func CreateBlog(b *model.Blog) {
dbClient.Create(&blog{...})
...
}
接口适配层的外层是处在最外层的框架和驱动层
import "gorm.io/driver/mysql"
import "gorm.io/gorm"
type blog struct { // Data Object
Content string `gorm:"..."` // 本层的数据库 ORM 如果替换,此处的 tag 也需要随之改变
}
type MySQLClient struct { DB *gorm.DB }
func New(...) { gorm.Open(...) ... }
func Create(...)
...
2.2 洋葱架构
2.3 六边形架构
2.4 COLA架构
03
├── adapter // Adapter层,适配各种框架及协议的接入,比如:Gin,tRPC,Echo,Fiber 等 ├── application // App层,处理Adapter层适配过后与框架、协议等无关的业务逻辑 │ ├── consumer //(可选)处理外部消息,比如来自消息队列的事件消费 │ ├── dto // App层的数据传输对象,外层到达App层的数据,从App层出发到外层的数据都通过DTO传播 │ ├── executor // 处理请求,包括command和query │ └── scheduler //(可选)处理定时任务,比如Cron格式的定时Job ├── domain // Domain层,最核心最纯粹的业务实体及其规则的抽象定义 │ ├── gateway // 领域网关,model的核心逻辑以Interface形式在此定义,交由Infra层去实现 │ └── model // 领域模型实体 ├── infrastructure // Infra层,各种外部依赖,组件的衔接,以及domain/gateway的具体实现 │ ├── cache //(可选)内层所需缓存的实现,可以是Redis,Memcached等 │ ├── client //(可选)各种中间件client的初始化 │ ├── config // 配置实现 │ ├── database //(可选)内层所需持久化的实现,可以是MySQL,MongoDB,Neo4j等 │ ├── distlock //(可选)内层所需分布式锁的实现,可以基于Redis,ZooKeeper,etcd等 │ ├── log // 日志实现,在此接入第三方日志库,避免对内层的污染 │ ├── mq //(可选)内层所需消息队列的实现,可以是Kafka,RabbitMQ,Pulsar等 │ ├── node //(可选)服务节点一致性协调控制实现,可以基于ZooKeeper,etcd等 │ └── rpc //(可选)广义上第三方服务的访问实现,可以通过HTTP,gRPC,tRPC等 └── pkg // 各层可共享的公共组件代 |
├── infrastructure │ ├── cache │ │ └── redis.go // Redis 实现的缓存 │ ├── client │ │ ├── kafka.go // 构建 Kafka client │ │ ├── mysql.go // 构建 MySQL client │ │ ├── redis.go // 构建 Redis client(cache和distlock中都会用到 Redis,统一在此构建) │ │ └── zookeeper.go // 构建 ZooKeeper client │ ├── config │ │ └── config.go // 配置定义及其解析 │ ├── database │ │ ├── dataobject.go // 数据库操作依赖的数据对象 │ │ └── mysql.go // MySQL 实现的数据持久化 │ ├── distlock │ │ ├── distributed_lock.go // 分布式锁接口,在此是因为domain/gateway中没有直接需要此接口 │ │ └── redis.go // Redis 实现的分布式锁 │ ├── log │ │ └── log.go // 日志封装 │ ├── mq │ │ ├── dataobject.go // 消息队列操作依赖的数据对象 │ │ └── kafka.go // Kafka 实现的消息队列 │ ├── node │ │ └── zookeeper_client.go // ZooKeeper 实现的一致性协调节点客户端 │ └── rpc │ ├── dataapi.go // 第三方服务访问功能封装 │ └── dataobject.go // 第三方服务访问操作依赖的数据对象 |
// Adapter 层 router.go,路由入口
import (
"mybusiness.com/blog-api/application/executor" // 向内依赖 App 层
"github.com/gin-gonic/gin"
)
func NewRouter(...) (*gin.Engine, error) {
r := gin.Default()
r.GET("/blog/:blog_id", getBlog)
...
}
func getBlog(...) ... {
// b's type: *executor.BlogOperator
result := b.GetBlog(blogID)
// c's type: *gin.Context
c.JSON(..., result)
}
如代码所体现,Gin 框架的内容会被全部限制在 Adapter 层,其他层不会感知到该框架的存在。
// App 层 executor/blog_operator.go
import "mybusiness.com/blog-api/domain/gateway" // 向内依赖 Domain 层
type BlogOperator struct {
blogManager gateway.BlogManager // 字段 type 是接口类型,通过 Infra 层具体实现进行依赖注入
}
func (b *BlogOperator) GetBlog(...) ... {
blog, err := b.blogManager.Load(ctx, blogID)
...
return dto.BlogFromModel(...) // 通过 DTO 传递数据到外层
}
// Domain 层 gateway/blog_manager.go
import "mybusiness.com/blog-api/domain/model" // 依赖同层的 model
type BlogManager interface { //定义核心业务逻辑的接口方法
Load(...) ...
Save(...) ...
...
}
Domain 层是核心层,不会依赖任何外层组件,只能层内依赖。这也保障了 Domain 层的纯粹,保障了整个软件系统的可维护性。
// Infrastructure 层 database/mysql.go
import (
"mybusiness.com/blog-api/domain/model" // 依赖内层的 model
"mybusiness.com/blog-api/infrastructure/client" // 依赖同层的 client
)
type MySQLPersistence struct {
client client.SQLClient // client 中已构建好了所需客户端,此处不用引入 MySQL, gorm 相关依赖
}
func (p ...) Load(...) ... { // Domain 层 gateway 中接口方法的实现
record := p.client.FindOne(...)
return record.ToModel() // 将 DO(数据对象)转成 Domain 层 model
}
04
参考文献:
[1] Robert C. Martin, The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html (2012)
[2] Andrew Gordon, Clean Architecture, https://www.andrewgordon.me/posts/Clean-Architecture/ (2021)
[3] Pablo Martinez, Hexagonal Architecture, there are always two sides to every story, https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c (2021)
[4] 张建飞, COLA 4.0:应用架构的最佳实践, https://blog.csdn.net/significantfrank/article/details/110934799 (2022)
[5] Jeffrey Palermo, The Onion Architecture, https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ (2008)
以上是本次分享全部内容,欢迎大家在评论区分享交流。如果觉得内容有用,欢迎转发~
欢迎在评论区聊一聊你的看法。在4月4日前将你的评论记录截图,发送给腾讯云开发者公众号后台,可领取腾讯云「开发者春季限定红包封面」一个,数量有限先到先得😄。我们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月4日中午12点开奖。快邀请你的开发者朋友们一起来参与吧!
最近微信改版啦
很多开发者朋友反馈收不到我们更新的文章
大家可以关注并点亮星标
🥹不再错过小云的知识速递🥹