当前位置:首页 > 技术文章 > 正文内容

模块化golang工程_go模块化调用

为什么要模块化

微服务本身是比较彻底的模块化方案,但是在部分场景下微服务不是最好的方案,例如:

  • 事务比较复杂
  • 希望内存占用尽量低(比如客户就给1c2g)
  • 数据库连接数量已经过大,不希望引入中间件去解决连接数问题
  • 运维能力不足
  • 开源项目,希望便于部署

在这些条件下,单服务+模块化变成了最好的方案。

模块化方式对比

作为云原生的默认语言,微服务化似乎应该是与生俱来的能力。相比其他语言,golang 的模块化方案被较少的讨论。

对于模块的需求,我们大致上有: - 可以比较方便的载入,最好能是运行时的 - 可以卸载 - 可以有能力去在某次业务逻辑中启用和禁用

golang官方实际有模块化方案:go plugin。有如下特点:

  • - 1.8版本引入
  • - 模块被打包为 so 文件,用代码动态加载
  • - 只能加载,不能卸载
  • - 主程序与plugin的共同依赖包的版本必须一致
  • - 如果采用mod=vendor构建,那么主程序和plugin必须基于同一个vendor目录构建
  • - 主程序与plugin使用的编译器版本必须一致

这些条件中,在常见的业务开发中,最难做到的就是依赖版本一致 假设主程序与模块都使用了内部 log库v1,现在 v1 提供了升级 v1.1,某模块需要这个升级。也就意味着主程序和所有的模块必须一起升级才能解决这个问题。如果有几百个模块呢。。。

因此 go plugin 这么久了也很少有人使用这样的方案去做模块化,有兴趣的可以看这个例子:
https://github.com/pingcap/tidb/blob/master/docs/design/2018-12-10-plugin-framework.md

不用 go plugin,还有什么其他方案吗?


github.com/hashicorp/go-plugin 使用了“假微服务”方案,通过子主进程进行模块化启动,通信使用 go 喜闻乐见的 grpc。这个方案有效的解决了运行时、卸载、依赖版本的问题。 但是他引入了新的问题:性能。 假设一个调用需要调用其他 1 个模块的方法,传了指针进去。在 go 层面只占用了一块内存,经由 grpc 传输后,内存占用变成了 3 块,也就是说一个调用内存就要翻 3 倍。普通业务开发其实到也影响不大,在涉及大流量网络开发中,这个量级可能太吓人了。

编辑

image


对性能要求比较高,或需要内存占用比较低的场景中,这个方案不太行。如果要使用 rpc,为什么不真的使用微服务呢,毕竟微服务体系下是另一套完整的生态了。监控、链路、编排等各类需求都有完整便捷的解决方案。

如果用 go mod 作为模块化方案,优势有:

  • - 完全支持多版本
  • - 没有学习成本
  • - 没有额外的性能开销

相对的,缺点有:

  • - 不可能运行时载入
  • - 不能卸载
  • - 编译的时候不能做单元测试
  • - 分支管理不能按照普通的方式来进行
  • - 配置管理需要升级(普通的配置方式对模块化不够灵活)
  • - 不能支持 swagger

go mod模块化实现细节

模块化方案是微服务的前奏,如果模块化成功,起码有了向微服务发展的基础。 从这个角度出发,模块化方案就必须满足一些要求:

  • - 依赖 interface 而不是指针
  • - 不使用导出变量的方式使用单例模式,尽量控制模块权限
  • - 每个模块的配置应该是独立的
  • - 尽管不要求运行时载入和卸载,起码载入和卸载应该很方便

因此,模块化方案在 go mod 的基础上,还应该有:

  • - 全面 ioc 的使用和支持,这样才能够依赖 interface,尽量控制模块权限
  • - 配置覆盖或组合能力,每个模块有独立的配置,同时可被其上层模块的配置覆盖,或提供配置组合能力。

模块的设计

模块的设计和微服务的划分可以对应,甚至可以更细粒度,毕竟还是单服务,没有微服务的劣势。 常见的,按照流程/对象或者抽象/实现的方式去划分,可以解决大部分的问题。

什么是好的模块(基本和微服务重叠):

  • - 可管理(动态装载和卸载,golang做不到)
  • - 原生可重用
  • - 可组合
  • - 无状态

一个模块设计的例子

需求:支付系统。

根据商品的不同,总价会有不同的折扣。但是折扣的计算比较复杂,会根据商品的不同属性计算不同折扣。例如商品上架时间、库存、供应商、批次等

第一次设计

有一个独立的服务,支付服务,来处理所有的需求。其会拉取商品服务的数据进行逻辑判断。

第一次模块化重构

抽取其中的商品拉取部分、数据库存储部分进行独立模块。现在逻辑部分在上,商品拉取、存储部分在下。

遵循一个原则:上层模块依赖下层,反之则不行

现在有2个模块:商品拉取、数据库存储

第二次模块化重构

逻辑部分在直接调用商品拉取,抽象其中部分为interface,商品拉取模块实现了这个interfcae。 同样,商品折扣的计算也独立出了一个模块,同样也实现了一个抽象的interface

遵循一个原则:只能依赖抽象而不是实现,实现依赖了本来应该依赖它的业务,依赖倒置了。

现在有3个模块:商品折扣计算、商品拉取、数据库存储

第三次模块化重构

折扣的计算包含多种多样,且可能随时增加或变更的逻辑。把它们全部分开成不同的实现,实现了相同的interface。用同一个管理器进行管理

遵循一个原则:适配器模式的使用,带来扩展的便利性

现在有3+n个模块:n个商品折扣计算、折扣计算管理、商品拉取、数据库存储

第四次模块化重构

支付的流程也成为了一个独立的模块,调用了一个通用的interface。折扣计算管理器实现了这个interface,因此将来不仅可以计算折扣,还通过扩展其他模块

遵循一个原则:main里面只剩下了di框架的初始化和配置,代码不超过20行了

现在有4+n个模块:n个商品折扣计算、支付模块、折扣计算管理、商品拉取、数据库存储

结果

经过4次重构后,系统从以前的一坨变成了一堆,也没有进行微服务拆分,但是谁都能看出来他的可维护性已经增加了太多。





更多精彩内容


纯异步事件驱动构建的微服务体系

我们为什么又又又又需要一个新的mq了

小团队的实用云原生指南


研发团队新鲜事儿,来公众号「迷路idea」找我一起探讨

相关文章

宽带客户收费管理系统--维修版(宽带售后服务)

宽带客户收费管理系统--维修版headerfooter《宽带客户收费管理系统——维修版》是一款适合宽带运营商使用的管理系统。软件主要包括以下功能:1.主要功能包括用户开户、收费录入、工单登记、故障处理...

(在线编辑DWG)网页CAD二开实现焊接符号绘制

前言在工程制图和制造领域,焊接符号(Welding Symbols)是用于表示焊缝类型、尺寸、位置以及工艺要求的标准化图形语言。广泛应用于机械设计、钢结构、船舶制造、压力容器等行业中,帮助技术人员理解...

Hutool JSONUtil巧妙过滤null值:JSON转Map数据清洗的终极方案

Hutool JSONUtil巧妙过滤null值:JSON转Map数据清洗的终极解决方案声明本文中的所有案例代码、配置仅供参考,如需使用请严格做好相关测试及评估,对于因参照本文内容进行操作而导致的任何...

PHP高级编程-回归原生态-函数式编程与数组

4.2.4 函数式编程与数组在函数式编程的世界里,针对集合的操作有三大类,分别是:映射、过滤和归约。虽然PHP是一门解释性脚本语言,并且支持面向过程编程和面向对象编程,但与函数式编程还是有很大区别的。...

2023 前端是否还需要 lodash ?(前端会被淘汰吗)

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!前言Lodash 是一个 JavaScri...

Vue中的路由配置常用属性(vue路由配置步骤)

router:路由页面跳转的核心库;引入路由:import VueRouter from 'vue-router'; 注册路由:const router = new VueRouter...