清晰架构(Clean Architecture)的Go微服务: 程序容器(Application Container)

清晰架构(Clean Architecture)的一个理念是隔离程序的框架,使框架不会接管你的应用程序,而是由你决定何时何地使用它们。在本程序中,我特意不在开始时使用任何框架,因此我可以更好地控制程序结构。只有在整个程序结构布局完成之后,我才会考虑用某些库替换本程序的某些组件。这样,引入的框架或第三方库的影响就会被正确的依赖关系所隔离。目前,除了logger,数据库,gRPC和Protobuf(这是无法避免的)之外,我只使用了两个第三方库ozzo-validation1和YAML2,而其他所有库都是Go的标准库。

你可以使用本程序作为构建应用程序的基础。你可能会问,那么本框架岂不是要接管整个应用程序吗?是的。但事实是,无论是你自建框架还是引进第三方框架,你都需要一个基本框架作为构建应用程序的基础。该基础需要具有正确的依赖性和可靠的设计,然后你可以决定是否引入其他库。你当然可以自己建立一个框架,但你最终可能会花费大量的时间和精力来完善它。你也可以使用本程序作为起点,而不是构建自己的项目,从而为你节省时间和精力。

程序容器是项目中最复杂的部分,是将应用程序的不同部分粘合在一起的关键组件。本程序的其他部分是直截了当且易于理解的,但这一部分不是。好消息是,一旦你理解了这一部分,那么整个程序就都在掌控之中。

容器包(“container” package)的组成部分:

容器包由五部分组成:

  1. “容器”(“container”)包:它负责创建具体类型并将它们注入其他文件。 顶级包中只有一个文件“container.go”,它定义容器的接口。

  2. “servicecontainer”子包:容器接口的实现。 只有一个文件“serviceContainer.go”,这是“容器”包的关键。 以下是代码。 它的起点是“InitApp”,它从文件中读取配置数据并设置日志记录器(logger)。
  type ServiceContainer struct {
      FactoryMap map[string]interface{}
      AppConfig  *config.AppConfig
  } 

  func (sc *ServiceContainer) InitApp(filename string) error {
      var err error
      config, err := loadConfig(filename)
      if err != nil {
          return errors.Wrap(err, "loadConfig")
      }
      sc.AppConfig = config
      err = loadLogger(config.Log)
      if err != nil {
          return errors.Wrap(err, "loadLogger")
      }
      return nil
  }
  // loads the logger
  func loadLogger(lc config.LogConfig) error {
      loggerType := lc.Code
      err := logFactory.GetLogFactoryBuilder(loggerType).Build(&lc)
      if err != nil {
          return errors.Wrap(err, "")
      }
      return nil
  }
  // loads the application configurations
  func loadConfig(filename string) (*config.AppConfig, error) {
      ac, err := config.ReadConfig(filename)
      if err != nil {
          return nil, errors.Wrap(err, "read container")
      }
      return ac, nil
  }
  1. “configs”子包:负责从YAML文件加载程序配置,并将它们保存到“appConfig”结构中以供容器使用。

  2. “logger”子包:它里面只有一个文件“logger.go”,它提供了日志记录器接口和一个“Log”变量来访问日志记录器。 因为每个文件都需要依赖记录,所以它需要一个独立的包来避免循环依赖。

  3. 最后一部分是不同类型的工厂(factory)。

    它的内部分层与应用层分层相匹配。 对于“usecase”和“dataservice”层,有“usecasefactory”和“dataservicefactory”。 另一个工厂是“datastorefactory”,它负责创建底层数据处理链接。 因为数据提供者可以是gRPC或除数据库之外的其他类型的服务,所以它被称为“datastorefactry”而不是“databasefactory”。 日志记录组件(logger)也有自己的工厂。

用例工厂(Use Case Factory):

对于每个用例,例如“registration”,接口在“usecase”包中定义,但具体类型在“usecase”包下的“registration”子包中定义。 此外,容器包中有一个对应的工厂负责创建具体的用例实例。 对于“注册(registration)”用例,它是“registrationFactory.go”。 用例与用例工厂之间的关系是一对一的。 用例工厂负责创建此用例的具体类型(concrete type)并调用其他工厂来创建具体类型所需的成员(member in a struct)。 最低级别的具体类型是sql.DBs和gRPC连接,它们需要被传递给持久层,这样才能访问数据库中的数据。

如果Go支持泛型,你可以创建一个通用工厂来构建不同类型的实例。 现在,我必须为每一层创建一个工厂。 另一个选择是使用反射(refection),但它有不少问题,因此我没有采用。

“Registration” 用例工厂(Use Case Factory):

每次调用工厂时,它都会构建一个新类型。以下是“注册(Registration)”用例创建具体类型的代码。 它是工厂方法模式(factory method pattern)的典型实现。 如果你想了解有关如何在Go中实现工厂方法模式的更多信息,请参阅此处3.

// Build creates concrete type for RegistrationUseCaseInterface
func (rf *RegistrationFactory) Build(c container.Container, appConfig *config.AppConfig, key string) (UseCaseInterface, error) {
    uc := appConfig.UseCase.Registration
    udi, err := buildUserData(c, &uc.UserDataConfig)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    tdi, err := buildTxData(c, &uc.TxDataConfig)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    ruc := registration.RegistrationUseCase{UserDataInterface: udi, TxDataInterface: tdi}

    return &ruc, nil
}

func buildUserData(c container.Container, dc *config.DataConfig) (dataservice.UserDataInterface, error) {
    dsi, err := dataservicefactory.GetDataServiceFb(dc.Code).Build(c, dc)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    udi := dsi.(dataservice.UserDataInterface)
    return udi, nil
}

数据存储工厂(Data store factory):

“注册(Registration)”用例需要通过数据存储工厂创建的数据库链接来访问数据库。 所有代码都在“datastorefactory”子包中。 我详细解释了数据存储工厂如何工作,请看这篇文章依赖注入(Dependency Injection)

数据存储工厂的当前实现支持两个数据库和一个微服务,MySql和CouchDB,以及gRPC缓存服务; 每个实现都有自己的工厂文件。 如果引入了新数据库,你只需添加一个新的工厂文件,并在以下代码中的“dsFbMap”中添加一个条目。


// To map "database code" to "database interface builder"
// Concreate builder is in corresponding factory file. For example, "sqlFactory" is in "sqlFactory".go
var dsFbMap = map[string]dsFbInterface{
    config.SQLDB:      &sqlFactory{},
    config.COUCHDB:    &couchdbFactory{},
    config.CACHE_GRPC: &cacheGrpcFactory{},
}

// DataStoreInterface serve as a marker to indicate the return type for Build method
type DataStoreInterface interface{}

// The builder interface for factory method pattern
// Every factory needs to implement Build method
type dsFbInterface interface {
    Build(container.Container, *config.DataStoreConfig) (DataStoreInterface, error)
}

//GetDataStoreFb is accessors for factoryBuilderMap
func GetDataStoreFb(key string) dsFbInterface {
    return dsFbMap[key]
}

以下是MySql数据库工厂的代码,它实现了上面的代码中定义的“dsFbInterface”。 它创建了MySql数据库链接。

容器内部有一个注册表(registry),用作数据存储工厂创建的链接(如DB或gRPC连接)的缓存,它们在整个应用程序创建一次。 无论何时需要它们,需首先从注册表中检索它,如果没有找到,则创建一个新的并将其放入注册表中。 以下是“Build”代码。

// sqlFactory is receiver for Build method
type sqlFactory struct{}

// implement Build method for SQL database
func (sf *sqlFactory) Build(c container.Container, dsc *config.DataStoreConfig) (DataStoreInterface, error) {
    key := dsc.Code
    //if it is already in container, return
    if value, found := c.Get(key); found {
        sdb := value.(*sql.DB)
        sdt := databasehandler.SqlDBTx{DB: sdb}
        logger.Log.Debug("found db in container for key:", key)
        return &sdt, nil
    }

    db, err := sql.Open(dsc.DriverName, dsc.UrlAddress)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    // check the connection
    err = db.Ping()
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    dt := databasehandler.SqlDBTx{DB: db}
    c.Put(key, db)
    return &dt, nil

}

Grpc Factory:

对于“listUser”用例,它需要调用gRPC微服务(缓存服务),而创建它的工厂是“cacheFactory.go”。 目前,数据服务的所有链接都是由数据存储工厂创建的。 以下是gRPC工厂的代码。 “Build”方法与“SqlFactory”的非常相似。


// DataStoreInterface serve as a marker to indicate the return type for Build method
type DataStoreInterface interface{}

// cacheGrpcFactory is an empty receiver for Build method
type cacheGrpcFactory struct{}

func (cgf *cacheGrpcFactory) Build(c container.Container, dsc *config.DataStoreConfig)
     (DataStoreInterface, error) {
    key := dsc.Code
    //if it is already in container, return
    if value, found := c.Get(key); found {
        return value.(*grpc.ClientConn), nil
    }
    //not in map, need to create one
    logger.Log.Debug("doesn't find cacheGrpc key=%v need to created a new one\n", key)

    conn, err := grpc.Dial(dsc.UrlAddress, grpc.WithInsecure())
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    c.Put(key, conn)
    return conn, err
}

Logger factory:

Logger有自己的子包名为“loggerfactory”,其结构与“datastorefactory”子包非常相似。 “logFactory.go”定义了日志记录器工厂构建器接口(builder interface)和映射(map)。 每个单独的日志记录器都有自己的工厂文件。 以下是日志工厂的代码:

// logger mapp to map logger code to logger builder
var logfactoryBuilderMap = map[string]logFbInterface{
    config.ZAP:    &ZapFactory{},
    config.LOGRUS: &LogrusFactory{},
}

// interface for logger factory
type logFbInterface interface {
    Build(*config.LogConfig) error
}

// accessors for factoryBuilderMap
func GetLogFactoryBuilder(key string) logFbInterface {
    return logfactoryBuilderMap[key]
}

以下是ZAP工厂的代码。 它类似于数据存储工厂。 只有一个区别。 由于记录器创建功能仅被调用一次,因此不需要注册表。

// receiver for zap factory
type ZapFactory struct{}

// build zap logger
func (mf *ZapFactory) Build(lc *config.LogConfig) error {
    err := zap.RegisterLog(*lc)
    if err != nil {
        return errors.Wrap(err, "")
    }
    return nil
}

配置文件:

配置文件使你可以全面了解程序的整体结构:

上图显示了文件的前半部分。 第一部分是它支持的数据库配置; 第二部分是带有gRPC的微服务; 第三部分是它支持的日志记录器; 第四部分是本程序在运行时使用的日志记录器

下图显示了文件的后半部分。 它列出了应用程序的所有用例以及每个用例所需的数据服务。

配置文件中应保存哪些数据?

不同的组件具有不同的配置项,一些组件可能具有许多配置项,例如日志记录器。 我们不需要在配置文件中保存所有配置项,这可能使其太大而无法管理。 通常我们只需要保存需要在运行时更改的选项或者可以在不同环境中(dev, prod, qa)值不同的选项。

设计是如何进化的?

容器包里似乎有太多东西,问题是我们是否需要所有这些?如果你不需要所有功能,我们当然可以简化它。当我开始创建它时,它非常简单,我不断添加功能,最终它才越来越复杂。

最开始时,我只是想使用工厂方法模式来创建具体类型,没有日志记录,没有配置文件,没有注册表。

我从用例和数据存储工厂开始。最初,对于每个用例,都会创建一个新的数据库链接,这并不理想。因此,我添加了一个注册表来缓存所有连接,以确保它们只创建一次。

然后我发现(我从这里获得了一些灵感?)将所有配置信息放在一个文件中进行集中管理是个好主意,这样我就可以在不改变代码的情况下进行更改。
我创建了一个YAML文件(appConfig [type] .yaml)和“appConfig.go”来将文件中的内容加载到应用程序配置结构(struct) - “appConfig”中并将其传递给工厂构建器(factory builder)。 “[type]”可以是“prod”,“dev”,“test”等。配置文件只加载一次。目前,它没有使用任何第三方库,但我想将来切换到Vipe?,因为它可以支持从配置服务器中动态重新加载程序配置。要切换到Vipe,我只需要更改一个文件“appConfig.go”。

对于日志记录,整个程序我只想要一个logger实例,这样我就可以为整个程序设置相同的日志配置。我在容器内创建了一个日志记录器包。我还尝试了不同的日志库来确定哪一个是最好的,然后我创建了一个日志工厂,以便将来更容易添加新的日志记录器。有关详细信息,请阅读日志管理?。

源程序:

完整的源程序链接 github: https://github.com/jfeng45/servicetmpl

索引:

[1] ozzo-validation

[2] YAML support for the Go language

[3][Golang Factory Method](https://stackoverflow.com/a/49714445)

[4][Go Microservice with Clean Architecture: Dependency Injection](https://jfeng45.github.io/posts/dependency_injection/)

[5] How I pass around shared resources (databases, configuration, etc) within Golang projects

[6][viper](https://github.com/spf13/viper)

[7][Go Microservice with Clean Architecture: Application Logging](https://jfeng45.github.io/posts/go_logging_and_error_handling/)

原文地址:https://www.cnblogs.com/code-craftsman/p/12173285.html

时间: 2024-11-08 23:09:08

清晰架构(Clean Architecture)的Go微服务: 程序容器(Application Container)的相关文章

微服务架构(一):什么是微服务

解析微服务架构系列文章将分几篇描述微服务的定义.特点.应用场景.企业集成架构的演进以及微服务转型思路和技术决策考虑等内容,并以IBM技术为例介绍如何实现微服务架构转型. 为什么需要微服务架构 "微服务"架构是近期软件应用领域非常热门的概念.让我们先来看看传统IT架构面临的一些问题: 使用传统的整体式架构(Monolithic Architecture)应用开发系统,如CRM.ERP等大型应用,随着新需求的不断增加,企业更新和修复大型整体式应用变得越来越困难: 随着移动互联网的发展,企业

(转)微服务架构 互联网保险O2O平台微服务架构设计

http://www.cnblogs.com/Leo_wl/p/5049722.html 微服务架构 互联网保险O2O平台微服务架构设计 关于架构,笔者认为并不是越复杂越好,而是相反,简单就是硬道理也提现在这里.这也是微服务能够流行的原因,看看市场上曾经出现的服务架构:EJB.SCA.Dubbo等等,都比微服务先进,都比微服务功能完善,但它们都没有微服务这么深入民心,就是因为他们过于复杂.简单就是高科技,苹果手机据说专门有个团队研究如何能让用户更加简单的操作.大公司都是由小公司发展起来的,如果小

微服务架构探讨及甲骨文中间件微服务技术解决方案

https://mp.weixin.qq.com/s/IWR_wIh2D-RmPuslR_JnXg 微服务架构探讨及甲骨文中间件微服务技术解决方案 2017-04-12 胡平 甲骨文开发者社区 随着传统企业受到互联网+的冲击,越来越多的企业都在面临业务转型,如何更好地贴近客户以获取更高的客户满意度,如何在企业内部加速供给侧改革,实现更好的供需平衡都是企业在业务转型中需要思考的问题.企业业务转型,离不开底层IT架构的支撑,所以最近很多很火的技术理念不断被大家所谈及,包括微服务架构.DevOps开发

从经典架构项目中透析微服务架构的核心概念和充血模型

微服务架构和SOA区别 微服务现在辣么火,业界流行的对比的却都是所谓的Monolithic单体应用,而大量的系统在十几年前都是已经是分布式系统了,那么微服务作为新的理念和原来的分布式系统,或者说SOA(面向服务架构)是什么区别呢? 我们先看相同点: 需要Registry,实现动态的服务注册发现机制:需要考虑分布式下面的事务一致性,CAP原则下,两段式提交不能保证性能,事务补偿机制需要考虑:同步调用还是异步消息传递,如何保证消息可靠性?SOA由ESB来集成所有的消息:都需要统一的Gateway来汇

企业应用架构演化探讨:从微服务到Service Mesh

作者:李宁 来源:博云技术社区 / 博云研究院 当下微服务的实践方案中,Spring Cloud,Dubbo作为主流的落地方案,在企业应用架构中发挥越来越重要的作用.本文探讨企业应用架构如何从微服务架构向Service Mesh架构演化,并形成落地方案.需要特别说明:本文讨论的架构目前适用于普通的企业级应用,其他行业(例如互联网)需要进一步扩展. 在讨论之前,我们需要明确一个事实:企业应用一定是围绕业务进行的. 无论采用什么的架构落地,都是为了更好的为应用业务进行服务.从企业应用的特性考虑,主要

Re:从0开始的微服务架构--(二)快速快速体验微服务架构?--转

原文地址:https://mp.weixin.qq.com/s/QO1QDQWnjHZp8EvGDrxZvw 这是专题的第二篇文章,看看如何搭建一个简单模式的微服务架构. 记得好久之前看到一个大牛说过:如果单体架构都搞不好,就别搞微服务架构.乍一看,这句很有道理,后来发现这句话是不太对的,因为微服务架构的目的就是为了降低系统的复杂性,所以 微服务架构应该比单体架构更简单.更好实践才对. 这篇文章,我们就分享一下如何搭建一个 简单模式 的微服务架构. 什么是微服务架构的简单模式? 相对于大型互联网

.netcore下的微服务、容器、运维、自动化发布

微服务 1.1     基本概念 1.1.1       什么是微服务? 微服务架构是SOA思想某一种具体实现.是一种将单应用程序作为一套小型服务开发的方法,每种应用程序都在其自己的进程中运行,并采用轻量级的通讯机制(TCP)进行通信.这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署.这些服务的集中化管理已经是最少的,它们可以用不同的编程语言编写,并使用不同的数据存储技术. 1.1.2       为什么要用微服务? 1.1.2.1   微服务解决了什么问题? 在微服务的最佳实践

有容云-PPT | 当微服务遇见容器

编者注: 本文为10月29日有容云高级技术顾问龙淼在Docker Live时代线下系列-广州站中演讲的PPT,本次线下沙龙为有容云倾力打造Docker Live时代系列主题线下沙龙,每月一期畅聊容器技术生态,北京,深圳,广州,上海--有容云跨城带你起航!文中跟大家讨论了以Docker为代表.日益崛起的容器技术,与微服务又将擦出怎样的火花呢?尝鲜难免需要代价,我们将面临哪些挑战,如何迎接挑战,具体详情见以下PPT分享内容.

微服务SpringCloud容器化案例

前言 当我们在使用微服务的时候,那么有一个问题一定会困扰我们,那就是项目的测试和部署.因为在单体应用下,部署项目很简单,直接打包启动就可以了,而对于微服务来说,因为有各个组件的存在所以让测试和部署都变得很麻烦,而容器化是微服务的部署一把利剑. PS:本文不介绍具体docker使用的各种基础,以及微服务的各种基础,就是给出相应的案例,你可以根据这样的案例快速学会如何将你的微服务容器化. 实际案例 让我们先来看看,实现之后如果本地要进行测试,是多么方便. ? 只需要一个命令,就能将我们的服务都启动起