乐趣区

关于golang:事件驱动的微服务创建第三方库

本篇是我的事件驱动的微服务系列的第三篇,次要讲述如何在 Go 语言中创立第三方库。如果想要理解总体设计,请看第一篇 ” 事件驱动的微服务 - 总体设计 ”。
在 Go 语言中创立第三方库是为了共享程序,做起来并不艰难,不过你须要思考如下几个方面:

  • 第三方库的对外接口
  • 第三方库的内部结构
  • 如何解决配置参数
  • 如何裁减第三方库

咱们用日志做例子讲述如何创立第三方库。Go 语言有许多第三方日志库,它们各有优缺点。我在 ” 清晰架构(Clean Architecture)的 Go 微服务: 日志治理 ” 中讲到了“ZAP”是迄今为止我发现的最好的日志库,但它也不是美中不足,我在期待更好的库。不过我心愿未来替换库的时候不须要改代码或只有改很少的代码,当初的框架曾经可能反对这种替换。它的根底就是所有的日志调用都是通过通用接口(而不是某个第三方库的专用接口),这样只有创立日志库的操作是与具体库无关的(这部分代码是须要批改的),而其余日志库的操作是不须要批改代码的。

第三方库的对外接口

type Logger interface {Errorf(format string, args ...interface{})
    Fatalf(format string, args ...interface{})
    Fatal(args ...interface{})
    Infof(format string, args ...interface{})
    Info(args ...interface{})
    Warnf(format string, args ...interface{})
    Debugf(format string, args ...interface{})
    Debug(args ...interface{})
}

调用接口

下面就是日志库的接口,它的最重要的准则就是通用性,不能与任何特定的第三方日志库绑定。这个接口十分重要,它的稳定性是决定它是否能广泛应用的要害。作为码农,一个现实就是能像搭积木一样编程,这个口号曾经喊了几十年了,但没什么停顿,次要起因就是没有对立的服务接口,这个接口是须要跨语言的,而所有的服务都要有标准接口。这样,应用程序才可能是可插拔的。在多数畛域实现了这个现实,例如 Java 里的 JDBC,但它的局限性还是很显著的。例如,它只适宜 SQL 数据库,NoSQL 的接口就形形色色了;而且它只适宜 Java,在别的语言里就不实用了。

对日志来说,Java 里有一个 SLF4J,就是为了在 Java 里实现日志库的可插拔而创立的。但 Go 里没有相似的货色,因而我就本人写了一个。但因为是本人写的,就只能是一个比较简单的,本人用没问题,但不能成为一个规范。

创立实例接口

除了调用接口,当你创立日志库实例时,还须要另外的接口,那就是创立实例的接口。上面就是代码,你只须要调用 ”Build()” 函数,并把须要的配置参数传进来。

上面的代码不是日志库中的代码,而是 ” 领取服务 ” 调用日志库的代码。

func initLogger (lc *logConfig.Logging) error{log, err := logFactory.Build(lc)
    if err != nil {return errors.Wrap(err, "loadLogger")
    }
    logger.SetLogger(log)
    return nil
}

上面就是日志库中的“Build()”函数的代码

func Build(lc *config.Logging) (glogger.Logger, error) {
    loggerType := lc.Code
    l, err := GetLogFactoryBuilder(loggerType).Build(lc)
    if err != nil {return l, errors.Wrap(err, "")
    }
    return l, nil
}

在设计中一个让人比拟纠结的中央就是是否要把实例创立局部放到接口中。调用接口是必定要标准化,定义成通用接口。那么创立实例的函数呢?一方面仿佛应把它放到标准接口中,有了它,整个过程才残缺。但这样做扩充了接口范畴,而且实例创立(包含配置参数)自身就不是标准化的,把它纳入接口减少了接口的不稳定性。我最初还是决定把先它纳入接口,如果有问题当前再改。

配置参数定义

一旦要把创立实例纳入接口,那么把配置参数也纳入就牵强附会了。

上面就是在 ” 领取服务 ” 中对“glogger”库中的配置参数的定义

type Logging struct {
    // log library name
    Code string `yaml:"code"`
    // log level
    Level string `yaml:"level"`
    // show caller in log message
    EnableCaller bool `yaml:"enableCaller"`
}

第三方库的内部结构

我以前有一件事始终想不明确,就是有不少 Go 的第三方库都把很多文件放在根目录,甚至整个库就只有一个根目录(外面没有任何子目录),这样当文件多了时,就显得横七竖八,很难治理。为什么不能建几个子目录呢?当我也开始写第三方库时,终于找到了起因。

在解释之前,我先讲一下,什么是现实中的第三方库的目录构造。它的构造如下图(还是用日志做例子):

其中,因为 ”logger.go” 里有它的对外接口,能够把它放在根目录,这样当其它程序援用它时,只须要“import”根目录。其次,它能够反对多个日志库的实现,这样每个日志库能够创立一个目录,例如“Logrus”和“Zap”就是反对通用日志库的两个实现,它们的封装代码都在本人独自的目录里。

这外面最艰难的中央就是解决循环依赖的问题。因为它的接口是定义在根目录,而其它局部是要用到接口的,因而是要依赖根目录的,也就是说它的依赖方向从里向外的。“factory”里的代码是用来创立实例的,目录里的“factory.go” 里有一个函数 ”Build()”,原本也应该放到 ”logger.go” 里,这样应用程序就只用“import”第三方库的根目录就行了。但 ”Build()” 是要调用内层的日志库的工厂函数的,这样依赖关系就变成了从外到里,于是造成了循环依赖。我想了几种方法来建设子目录,但都不称心,最初发现必须把所有的文件都放在跟目录能力解决问题。当初终于晓得了为什么有那么多第三方库都这么做了。它的最大益处就是应用程序援用时只须要“import”一个包,比较简单。

但它的问题是目录外部没有任何构造,当文件不多时还能够承受,文件一多就基本没法治理。当要反对新的日志库时,也不晓得从哪下手。我最初还是把它改成有内部结构的,这样须要减少两个目录“factory”和“config”。但它的毛病就是当应用程序援用它时,总共须要须要三条“import”语句,还裸露了第三方库的内部结构。具体哪种计划更好,可能就见仁见智了。我自己当初还是感觉这样更好。

原本“config.go”和“factory.go”最好也是放在一个目录下,但这样也会造成循环依赖,因而只能把他们拆开寄存了。

如何解决配置参数:

日志库的配置参数和应用程序的配置参数如何协调是另一个难点。从一方面来讲,第三方库的配置参数的代码和解决逻辑应该是在第三方库里,这样能力保障日志局部的逻辑是残缺的,并集中在一个中央。另一方面,一个应用程序的所有参数应该对立存储在一个中央,它有可能存在一个文件里,也有可能是存储在代码里。当初的框架是反对把配置参数寄存在一个独自的文件里的,这仿佛是一个比拟好的办法。这样咱们就陷入了一个两难的地步。

解决的方法是把配置参数分成两个局部,一部分是配置参数的定义和逻辑,这部分由第三方库来实现。另一部分是参数寄存,这部分放在应用程序里,这样就保障了应用程序参数的集中管理。应用时能够让应用程序将参数传给第三方库,但由第三方库进行参数配置。

上面几段代码就是在 ” 领取服务 ” 中初始化 glogger 库的代码, 它是初始化整个程序容器的一部分,它在“app.go” 里。

上面的代码初始化程序容器,它先读取配置参数,而后分步初始化容器。

func InitApp(filename...string) (container.Container, error) {config, err := config.BuildConfig(filename...)
    if err != nil {return nil, errors.Wrap(err, "loadConfig")
    }
    err = initLogger(&config.LogConfig)
    if err != nil {return nil, err}
    return initContainer(config)
}

上面是从文件中读取配置参数(应用程序的所有参数,其中包含日志配置参数)的代码,它在“appConfig.go” 里。


func BuildConfig(filename ...string) (*AppConfig, error) {if len(filename) == 1 {return buildConfigFromFile(filename[0])
    } else {return BuildConfigWithoutFile()
    }
}

func buildConfigFromFile(filename string) (*AppConfig, error) {

    var ac AppConfig
    file, err := ioutil.ReadFile(filename)
    if err != nil {return nil, errors.Wrap(err, "read error")
    }
    err = yaml.Unmarshal(file, &ac)

    if err != nil {return nil, errors.Wrap(err, "unmarshal")
    }
    fmt.Println("appConfig:", ac)
    return &ac, nil
}

上面的代码初始化日志库,它把后面读到的参数传给日志库,并通过调用“Build()” 函数来取得合乎日志接口的具体实现。

func initLogger (lc *logConfig.Logging) error{log, err := logFactory.Build(lc)
    if err != nil {return errors.Wrap(err, "loadLogger")
    }
    logger.SetLogger(log)
    return nil
}

如何减少新的接口实现

当初的接口封装了两个反对通用日志接口的库,”zap” 和 ”Logrus”。当你须要减少一个新的日志库,例如 ”glog” 时,你须要实现以下操作。

第一,你须要批改”logFactory.go”, 在其中减少一个新的日志库选项。
上面是当初的代码:

const (
    LOGRUS string = "logrus"
    ZAP    string = "zap"
)

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

上面是批改后的代码:

const (
    LOGRUS string = "logrus"
    ZAP    string = "zap"
    GLOG    string = "glog"
)

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

第二,你须要在根目录下创立“glog”目录,它外面要蕴含两个文件。“glogFactory.go”和“glog.go”。其中“glogFactory.go”是工厂文件,与“logrusFactory.go”根本一样,这里就不具体讲了。“glog.go”次要是实现参数配置和日志库的初始化。

上面就是 logrus 的文件“logrus.go”。“glog.go”可参照这个来写。其中“RegisterLogrusLog()”函数是对 logrus 的通用配置,“customizeLogrusLogFromConfig()”是依据应用程序传过来的参数,进行有针对性的配置。

func RegisterLogrusLog(lc logconfig.LogConfig) (glogger.Logger, error) {
    //standard configuration
    log := logrus.New()
    log.SetFormatter(&logrus.TextFormatter{})
    log.SetReportCaller(true)
    //log.SetOutput(os.Stdout)
    //customize it from configuration file
    err := customizeLogrusLogFromConfig(log, lc)
    if err != nil {return nil, errors.Wrap(err, "")
    }
    //This is for loggerWrapper implementation
    //logger.Logger(&loggerWrapper{log})

    //SetLogger(log)
    return log, nil
}

// customizeLogrusLogFromConfig customize log based on parameters from configuration file
func customizeLogrusLogFromConfig(log *logrus.Logger, lc logconfig.LogConfig) error {log.SetReportCaller(lc.EnableCaller)
    //log.SetOutput(os.Stdout)
    l := &log.Level
    err := l.UnmarshalText([]byte(lc.Level))
    if err != nil {return errors.Wrap(err, "")
    }
    log.SetLevel(*l)
    return nil
}

论断:

下面讲了如果要创立一个第三方库须要做些什么,它用日志服务来做例子,次要的工作是创立一个通用的日志接口以及封装一个新的反对这个接口的日志库。创立其它的通用服务接口(例如 ” 数据库事务管理 ” 或 ” 音讯接口 ”)也和它相似。它次要蕴含两局部的代码,一个是通用接口,一个是具体实现的封装。具体实现能够当前逐步加多。

源程序:

残缺的源程序链接:

  • glogger
  • “ 领取服务 ”

索引:

1 事件驱动的微服务 - 总体设计

2 “ 清晰架构(Clean Architecture)的 Go 微服务: 日志治理 ”

3 “ 领取服务 ”

4 “zap”

5 “Logrus”

6 “glog”

7 “ 数据库事务管理 ”

8 “ 音讯接口 ”

退出移动版