基于Go-Kit的Golang整洁架构实践

如何用Golang实现简洁架构?本文介绍了基于Go-Kit实现简洁架构的尝试,通过示例介绍了简洁架构的具体实现。原文: Why is Go-Kit Perfect For Clean Architecture in Golang?

alt
简介

Go是整洁架构(Clean Architecture)的完美选择。整洁架构本身只是一种方法,并没有告诉我们如何构建源代码,在尝试用新语言实现时,认识到这点非常重要。

自从我有了使用Ruby on Rails的经验后,尝试了好几次编写第一个服务,而且我读过的大多数关于Go的整洁架构的文章都以一种非Go惯用的方式介绍结构布局。部分原因是这些例子中的包是根据层命名的——controllermodelservice等等……如果你有这些类型的包,这是第一个危险信号,告诉你应用程序需要重新设计。在Go中,包名[1]应该描述包提供了什么,而不是包含了什么。

alt

然后我开始了解go-kit,特别是它提供的发货示例[2],并决定在应用程序中实现相同的结构。后来,当我深入研究整洁架构(Clean Architecture)时,惊喜的发现go-kit方法是多么完美。

本文将介绍使用Go-Kit方法编写服务是如何符合整洁架构理念的。

整洁架构(Clean Architecture)

整洁架构(Clean Architecture)是由Bob大叔(Robert Martin)创建的一种软件架构设计。目标是分离关注点,允许开发人员封装业务逻辑,并使其独立于交付和框架机制。许多架构范例(如Onion和Hexagon架构)也有相同的目标,都是通过将软件划分成层来实现解耦。

alt

圆圈中的箭头表示依赖规则。如果在外部循环中声明了某些内容,则不得在内部循环代码中引用。它既适用于实际的源代码依赖关系,也适用于命名。内层不依赖于任何外层。

外层包含低级组件,如UI、DB、传输或任何第三方服务,都可以被认为是应用程序的细节或插件。其思想是,外层的变化一定不会引起内层的任何变化。

不同模块/组件之间的依赖关系可以描述如下:

alt

请注意,跨越边界的箭头只指向一个方向,边界后面的组件属于外层,包括controller、presenter和database。Interactor是实现BL的地方,可以将其视为用例层。

请注意Request ModelResponse Model。这些对象分别描述了内层需要和返回的数据。controller将请求(在web的情况下是HTTP请求)转换为请求模型(Request Model),presenter将响应模型(Response Model)格式化为可以由视图模型(View Model)呈现的数据。

还要注意接口,用于反转控制流以与依赖规则相对应。Interactor通过Boundary接口与presenter对话,并通过Entity Gateway接口与数据层对话。

这是整洁架构的主要思想,通过依赖注入分离不同的层,使用依赖反转反转控制流。Interactor(BL)和实体对传输和数据层一无所知。这一点很重要,因为如果我们改变了外层细节,内层就不会发生级联变化。

什么是Go-Kit?

Go kit是包的集合,可以帮助我们构建健壮、可靠、可维护的微服务。

对于来自Ruby on Rails的我来说,重要的是Go-Kit不是MVC框架。相反,它将应用程序分为三层:

  • Transport(传输)
  • Endpoint(端点)
  • Service(服务)
Transport

传输层是唯一熟悉交付机制(HTTP、gRPC、CLI…)的组件,这一点非常强大,因为我们可以通过提供不同的传输层来同时支持HTTP和CLI。

稍后我们将看到传输层是如何对应于上图中的controllerpresenter的。

Endpoint
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)

端点层表示应用程序中的单个RPC,将交付连接到BL。这是根据输入和输出实际定义用例的地方,在整洁架构术语中是Request ModelResponse Model

注意,端点是接收请求并返回响应的函数,都是interface{},是RequestModelResponseModel。理论上也可以用类型参数(泛型)来实现。

Service

服务层(interactor)是实现BL的地方。服务层不知道端点层,服务层和端点层都不知道传输域(比如HTTP)。

Go-Kit提供了创建服务器(HTTP服务器/gRPC服务器等)的功能。例如HTTP:

package http // under go-kit/kit/transport/http

type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error)
type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error

func NewServer(
  e endpoint.Endpoint,
  dec DecodeRequestFunc,
  enc EncodeResponseFunc,
  options ...ServerOption,
)
 *Server

  • DecodeRequestFunc将HTTP请求转换为 Request Model,并且
  • EncodeResponseFunc格式化 Response Model并将其编码到HTTP响应中。
  • 返回的 *server实现 http.Server(有 ServeHTTP方法)。

传输层使用这个函数来创建http.Server,解码器和编码器在传输中定义,端点在运行时初始化。

简短示例:(基于发货示例)

简易服务

我们将描述一个具有两个API的简单服务,用于从数据层创建和读取文章,传输层是HTTP,数据层只是一个内存映射。可以在这里找到GitHub源代码[3]

注意文件结构:

- inmem
  - articlerepo.go
- publishing
  - transport.go 
  - endpoint.go
  - service.go
  - formatter.go
- article
  - article.go

我们看看如何表示整洁架构的不同层。

  • article —— 这是实体层,不包含BL、数据层或传输层的知识。
  • inmem —— 这是数据层。
  • transport —— 这是传输层。
  • endpoint+ service —— 组成了边界+交互器。
从服务开始:
import (
  "context"
  "fmt"
  "math/rand"
 
  "github.com/OrenRosen/gokit-example/article"
)

type ArticlesRepository interface {
   GetArticle(ctx context.Context, id string) (article.Article, error)
   InsertArticle(ctx context.Context, thing article.Article) error
}

type service struct {
   repo ArticlesRepository
}

func NewService(repo ArticlesRepository) *service {
   return &service{
      repo: repo,
   }
}

func (s *service) GetArticle(ctx context.Context, id string) (article.Article, error) {
   return s.repo.GetArticle(ctx, id)
}

func (s *service) CreateArticle(ctx context.Context, artcle article.Article) (id string, err error) {
   artcle.ID = generateID()
   if err := s.repo.InsertArticle(ctx, artcle); err != nil {
      return "", fmt.Errorf("publishing.CreateArticle: %w", err)
   }
   
   return artcle.ID, nil
}

func generateID() string {
  // code emitted
}

服务对交付和数据层一无所知,它不从外层(HTTP、inmem…)导入任何东西。BL就在这里,你可能会说这里没有真正的BL,这里的服务可能是冗余的,但需要记住这只是一个简单示例。

实体
package article

type Article struct {
   ID    string
   Title string
   Text  string
}

实体只是一个DTO,如果有业务策略或行为,可以添加到这里。

端点

endpoint.go定义了服务接口:

type Service interface {
   GetArticle(ctx context.Context, id string) (article.Article, error)
   CreateArticle(ctx context.Context, thing article.Article) (id string, err error)
}

然后为每个用例(RPC)定义一个端点。例如,对于获取文章:

type GetArticleRequestModel struct {
   ID string
}

type GetArticleResponseModel struct {
   Article article.Article
}

func MakeEndpointGetArticle(s Service) endpoint.Endpoint {
   return func(ctx context.Context, request interface{}) (response interface{}, err error) {
      req, ok := request.(GetArticleRequestModel)
      if !ok {
         return nil, fmt.Errorf("MakeEndpointGetArticle failed cast request")
      }
      
      a, err := s.GetArticle(ctx, req.ID)
      if err != nil {
         return nil, fmt.Errorf("MakeEndpointGetArticle: %w", err)
      }
      
      return GetArticleResponseModel{
         Article: a,
      }, nil
   }
}

注意如何定义RequestModelResponseModel,这是RPC的输入/输出。其思想是,可以看到所需数据(输入)和返回数据(输出),甚至无需读取端点本身的实现,因此我认为端点代表单个RPC。服务具有实际触发BL的方法,但是端点是RPC的应用定义。理论上,一个端点可以触发多个BL方法。

传输

transport.go注册HTTP路由:

type Router interface {
   Handle(method, path string, handler http.Handler)
}

func RegisterRoutes(router *httprouter.Router, s Service) {
   getArticleHandler := kithttp.NewServer(
      MakeEndpointGetArticle(s),
      decodeGetArticleRequest,
      encodeGetArticleResponse,
   )
   
   createArticleHandler := kithttp.NewServer(
      MakeEndpointCreateArticle(s),
      decodeCreateArticleRequest,
      encodeCreateArticleResponse,
   )
   
   router.Handler(http.MethodGet, "/articles/:id", getArticleHandler)
   router.Handler(http.MethodPost, "/articles", createArticleHandler)
}

传输层通过MakeEndpoint函数在运行时创建端点,并提供用于反序列化请求的解码器和用于格式化和编码响应的编码器。

例如:

func decodeGetArticleRequest(ctx context.Context, r *http.Request) (request interface{}, err error) {
   params := httprouter.ParamsFromContext(ctx)
   return GetArticleRequestModel{
      ID: params.ByName("id"),
   }, nil
}

func encodeGetArticleResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
   res, ok := response.(GetArticleResponseModel)
   if !ok {
      return fmt.Errorf("encodeGetArticleResponse failed cast response")
   }
   
   formatted := formatGetArticleResponse(res)
   w.Header().Set("Content-Type""application/json")
   return json.NewEncoder(w).Encode(formatted)
}

func formatGetArticleResponse(res GetArticleResponseModel) map[string]interface{} {
  return map[string]interface{}{
    "data"map[string]interface{}{
      "article"map[string]interface{}{
        "id":    res.Article.ID,
        "title": res.Article.Title,
        "text":  res.Article.Text,
      },
    },
  }
}

你可能会问,为什么要使用另一个函数来格式化article,而不是在article实体上添加JSON标记?

这是个非常重要的问题。在article实体上添加JSON标记意味着article知道它是如何格式化的。虽然没有显式导入到HTTP,但打破了抽象,使实体包依赖于传输层。

例如,假设你想将对客户端的响应从"title"更改为"header",此更改仅涉及传输层。但是,如果此需求导致需要更改实体,则意味着该实体依赖于传输层,这就破坏了简洁架构原则。

我们看看这个简单应用的依赖关系图:

alt

哇,你一定注意到了它们的相似性!article实体没有依赖关系(只有向内箭头)。外层,transport和inmem,只有指向BL和实体内层的箭头。

一切都和转换有关

跨界就是不同层次语言之间的转换。

BL层只使用应用语言,也就是说,只知道实体(没有HTTP请求或SQL查询)。为了跨越边界,流中的某个组件必须将应用语言转换为外层语言。

在传输层,有解码器(将HTTP请求转换为RequestModel的应用语言)和编码器(将应用语言ResponseModel转换为HTTP响应)。

数据层实现了repo,在我们的例子中是inmem。在另一种情况下,我们可能会让sql包负责将应用语言转换为SQL语言(查询和原始结果)。

"ing"包

你可能会说传输和服务不应该在同一个包中,因为它们位于不同的层,这是一个正确的论点。我从go-kit的shipping例子中取了一个例子,含有这种设计,ing包包含了传输/端点/服务,我发现从长远来看非常方便。话虽如此,如果我现在写的话,可能会用不同的包。

最后关于"尖叫架构(Screaming Architecture)"的一句话

Go非常适合简洁架构的另一个原因是包的命名及其思想。尖叫架构(Screaming Architecture) 和构建应用程序有关,以便应用程序的意图显而易见。在Ruby On Rails中,当查看结构时,就知道它是用Ruby On Rails框架编写的(控制器、模型、视图……)。在我们的应用程序中,当查看结构时,可以看出这是一个关于文章的应用程序,有发布用例,并使用inmem数据层。

总结

简洁架构只是一种方法,并不会告诉你如何构建源代码,其实现艺术在于了解所用语言的使用惯例和工具。希望这篇文章对你有所帮助,重要的是要意识到,那些争论设计问题解决方案的文章并不总是对的,当然也包括这篇😀


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

参考资料
[1]

Package names: https://go.dev/blog/package-names

[2]

shipping example in go-kit: https://github.com/go-kit/examples/tree/master/shipping

[3]

gokit-example: https://github.com/OrenRosen/gokit-example

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/371799.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于python+控制台的车辆信息管理系统

基于python控制台的车辆信息管理系统 一、系统介绍二、效果展示三、其他系统实现四、获取源码 一、系统介绍 打印功能菜单、添加车辆信息、删除车辆信息、修改车辆信息、显示车辆信息、退出系统,并且需要接收用户的输入,在根据输入内容调用相应函数实现…

深度学习介绍

对于具备完善业务逻辑的任务,大多数情况下,正常的人都可以给出一个符合业务逻辑的应用程序。但是对于一些包含超过人类所能考虑到的逻辑的任务,例如面对如下任务: 编写一个应用程序,接受地理信息、卫星图像和一些历史…

指针的深入理解(四)

这节主要讨论sizeof和strlen的区别,以及一些理解题。 sizeof 求的是对象的大小,深入理解一点就是:这个对象,他一定有一块对应的内存空间。求的就是这一块内存空间。 strlen 只能用来求字符串, 求取的是字符串的长度。…

Unity中blendtree和state间的过渡

混合树状态之间的过渡 如果属于此过渡的当前状态或下一状态是混合树状态,则混合树参数将出现在 Inspector 中。通过调整这些值可预览在混合树值设置为不同配置时的过渡表现情况。 如果混合树包含不同长度的剪辑,您应该测试在显示短剪辑和长剪辑时的过渡表…

Mocaverse NFT 概览与数据分析

作者:stellafootprint.network 编译:mingfootprint.network 数据源:Mocaverse NFT Collection Dashboard Mocaverse 是 Animoca Brands 推出的专属 NFT(非同质化代币)系列,包含 8,888 个独特的 "M…

深入理解TCP网络协议(3)

目录 1.前言 2.流量控制 2.阻塞控制 3.延时应答 4.捎带应答 5.面向字节流 6.缓冲区 7.粘包问题 8.TCP异常情况 9.小结 1.前言 在前面的博客中,我们重点介绍了TCP协议的一些属性,有连接属性的三次握手和四次挥手,还有保证数据安全的重传机制和确认应答,还有为了提高效率…

2024美赛E题成品论文22页详细讲解+完整代码数据汇总

E题社区抗灾能力综合评估与决策模型研究 (完整版在文末) 摘要:社区抗灾能力的提升对于灾害风险管理至关重要。本研究基于机器学 习方法,构建了社区抗灾能力预测模型,以评估社区在灾害事件中的表现。首先, 我…

在maven环境中使用GraalVM来构建本地原生应用程序(一)构建本地可执行文件

文章目录 前言一、GraalVM安装二、初步使用三、踩坑记录1、JSON转换问题2、反射、资源、jni的调用问题3、HTTPS调用问题4、Linux下CPU架构问题5、Linux下GLIBC版本的问题6、部分Windows系统无法缺少相关的库文件 总结 前言 随着Java17的更新,jdk又推出了一个GraalV…

【lesson10】高并发内存池细节优化

文章目录 大于256KB的大块内存申请问题大于256KB的大块释放申请问题使用定长内存池脱离使用new释放对象时优化为不传对象大小完整版代码Common.hObjectPool.hThreadCache.hThreadCache.cppConcurrentAlloc.hCentralCache.hCentralCache.cppPageCache.hPageCache.cpp 大于256KB的…

SpringBoot中数据库的连接及Mybatis的配置和使用

目录 1 在pom.xml中引入相关依赖 2 对数据库进行配置 2.1 配置application.yml 2.2 idea连接数据库 (3.2.1有用到) 3 Mybatis的使用 3.1 测试文件的引入 3.2 使用 3.2.1 使用注解(有小技巧(✪ω✪)) 3.2.2 使用动态sql 1 在pom.xml中引入相关依赖 <dependencies&g…

【DDD】学习笔记-EAS 的整体架构实践

为了得到系统的整体架构&#xff0c;我们还欠缺什么呢&#xff1f;所谓“架构”&#xff0c;是“以组件、组件之间的关系、组件与环境之间的关系为内容的某一系统的基本组织结构&#xff0c;以及指导上述内容设计与演化的原则”。之所以要确定系统的组件、组件关系以及设计与演…

线上编程答疑解惑回顾,初学编程中文编程在线屏幕共享演示

线上编程答疑解惑回顾&#xff0c;初学编程中文编程在线屏幕共享演示 一、学编程过程中有不懂的怎么办&#xff1f; 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载——常用工具下载——编…

基于深度学习的SSVEP分类算法简介

基于深度学习的SSVEP分类算法简介 1、目标与范畴2、深度学习的算法介绍3、参考文献 1、目标与范畴 稳态视觉诱发电位&#xff08;SSVEP&#xff09;是指当受试者持续注视固定频率的闪光或翻转刺激时&#xff0c;在大脑枕-额叶区域诱发的与刺激频率相关的电生理信号。与P300、运…

【C/C++ 12】C++98特性

目录 一、命名空间 二、缺省参数 三、函数重载 四、引用 五、内联函数 六、异常处理 一、命名空间 在C/C项目中&#xff0c;存在着大量的变量、函数和类&#xff0c;这些变量、函数和类都存在于全局作用域中&#xff0c;可能会导致命名冲突。 使用命名空间的目的就是对…

Gateway API 实践之(七)FSM Gateway 的负载均衡算法

FSM Gateway 流量管理策略系列&#xff1a; 故障注入黑白名单访问控制限速重试会话保持健康检查负载均衡算法TLS 上游双向 TLS 在微服务和 API 网关架构中&#xff0c;负载均衡是至关重要的&#xff0c;它确保每个服务实例都能平均地处理请求&#xff0c;同时也为高可用性和故…

2024.2.4 模拟实现 RabbitMQ —— 实现核心类

目录 引言 创建 Spring Boot 项目 编写 Exchange 实体类 编写 Queue 实体类 编写 Binding 实体类 编写 Message 实体类 引言 上图为模块设计图 此处实现核心类为了简便&#xff0c;我们引用 Lombok&#xff08;可点击下方链接了解 Lombok 的使用&#xff09; IDEA 配置 L…

【npm】修改npm全局安装包的位置路径

问题 全局安装的默认安装路径为&#xff1a;C:\Users\admin\AppData\Roaming\npm&#xff0c;缓存路径为&#xff1a;C:\Users\admin\AppData\Roaming\npm_cache&#xff08;其中admin为自己的用户名&#xff09;。 由于默认的安装路径在C盘&#xff0c;太浪费C盘内存啦&#…

C语言之数据在内存中的存储

目录 1. 整数在内存中的存储2. 大小端字节序和字节序判断什么是大小端&#xff1f;为什么有大小端&#xff1f;练习1练习2练习3练习4练习5练习6 3. 浮点数在内存中的存储浮点数存的过程浮点数取得过程练习题解析 1. 整数在内存中的存储 在讲解操作符的时候&#xff0c;我们已经…

算法学习——华为机考题库7(HJ41 - HJ45)

算法学习——华为机考题库7&#xff08;HJ41 - HJ45&#xff09; HJ41 称砝码 描述 现有n种砝码&#xff0c;重量互不相等&#xff0c;分别为 m1,m2,m3…mn &#xff1b; 每种砝码对应的数量为 x1,x2,x3…xn 。现在要用这些砝码去称物体的重量(放在同一侧)&#xff0c;问能称…

前端 - 基础 列表标签 - 自定义列表 详解

使用场景 &#xff1a; 常用于对术语或名词进行解释和描述&#xff0c;定义列表的列表前没有任何项目符号。 在 HTML 标签中&#xff0c; < dl > 标签用于定义 描述列表 &#xff08; 或定义列表 &#xff09; 该标签会与 <dt> ( 定义项目/名字 ) 和 <dd…