进行软件配置

地鼠与标志


大家好! 在Go上进行了五年编程之后,我发现自己相当
特定程序配置方法的热心支持者。 在这
我将尝试在文章中揭示他的主要思想,并分享一小部分
实现这些想法的图书馆。


显然,本文是非常主观的,并不假装是客观的
真相 但是,我希望它对社区有用,并有助于减少
在这样琐碎的任务上花费的时间。


你在说什么


一般来说,我认为配置是对我们变量的定义
我们可以在运行时从外部获取其值的程序。
它可以是参数或命令行参数,环境变量,
存储在磁盘上或网络上任何地方的配置文件,数据库表
数据等等。


由于Go是一种高度静态的类型化语言,因此我们希望
确定并获取此类变量的值,并考虑其类型。


有大量的开放源代码库甚至框架,
解决这类问题。 他们中的大多数代表了自己的愿景。
如何做到的。


我想谈谈一种不太常见的程序配置方法。
而且,在我看来,这种方法最简单。


flag


是的,这不是一个玩笑,我真的想引起您的注意
著名的Go标准库包。


乍一看, flag是用于处理命令参数的工具
线,仅此而已。 但是此包也可以用作接口。
确定我们程序参数。 在讨论的方法中
flag主要用于这种方式。


如上所述,我们希望输入类型的参数。
flag包提供了对大多数基本类型执行此操作的能力。
flag.String()flag.Int()甚至是flag.Duration()


对于更复杂的类型,例如[]stringtime.Time有一个接口
flag.Value ,它允许您描述flag.Value从中获取参数的值
字符串表示


例如,类型为time.Time的参数可以这样实现:


 // TimeValue is an implementation of flag.Value interface. type TimeValue struct { P *time.Time Layout string } func (t *TimeValue) Set(s string) error { v, err := time.Parse(t.Layout, s) if err == nil { (*tP) = v } return err } func (t *TimeValue) String() string { return tPFormat(t.Layout) } 

软件包的重要属性是它在标准库中的存在- flag
配置程序的标准方法 ,这意味着其可能性
不同项目和库之间的使用率高于其他项目和库
社区中的图书馆。


为什么不使用flag


在我看来,使用和存在其他库的原因有两个:


  • 不仅从命令行读取参数
  • 我想构造参数

例如,如果从文件中读取参数,则一切都差不多了(
稍后),然后关于结构参数,直接说几句话
现在。


我认为,这不是确定配置的最佳方法
程序作为结构,其字段可以是其他结构,因此
进一步:


 type AppConfig struct { Port int Database struct { Endpoint string Timeout time.Duration } ... } 

在我看来,这就是为什么使用和存在库的原因
允许您以这种方式使用配置的框架。


我认为flag不应该提供结构配置功能。
只需几行代码(或一个库)即可轻松实现
flagutil ,将在下面进行讨论)。


而且,如果您考虑一下,这种结构的存在会导致
组件之间的连通性。


结构构造


想法是定义参数而不管结构
程序,并尽可能靠近使用它们的地方-也就是说,
直接在包装级别。


假设我们为某些服务(数据库,
API或其他),称为yoogle


 package yoogle type Config struct { Endpoint string Timeout time.Duration } func New(c *Config) *Client { // ... } 

要填充yoogle.Config结构,我们需要一个函数
将结构字段注册到接收到的*flag.FlagSet


可以在yoogleyoogle或包中声明此类函数
yooglecfg (在第三方库的情况下,我们可以编写这样的函数
其他地方):


 package yooglecfg import ( "flag" "app/yoogle" ) func Export(flag *flag.FlagSet) *yoogle.Config { var c yoogle.Config flag.StringVar(&c.Endpoint, "endpoint", "https://example.com", "endpoint for our API", ) flag.DurationVar(&c.Timeout, "timeout", time.Second, "timeout for operations", ) return &c } 

为了消除对flag包的依赖,可以使用
必要的flag.FlagSet方法:


 package yooglecfg import "app/yoogle" type FlagSet interface { StringVar(p *string, name, value, desc string) } func Export(flag FlagSet) *yoogle.Config { var c yoogle.Config flag.StringVar(&c.Endpoint, "endpoint", "https://example.com", "endpoint for our API", ) return &c } 

并且如果配置取决于参数值(例如,
表示某种算法),函数yooglecfg.Export()可以返回
解析所有值调用构造函数
配置:


 package yooglecfg import "app/yoogle" type FlagSet interface { StringVar(p *string, name, value, desc string) } func Export(flag FlagSet) func() *yoogle.Config { var algorithm string flag.StringVar(&algorithm, "algorithm", "quick", "algorithm used to do something", ) var c yoogle.Config return func() *yoogle.Config { switch algorithm { case "quick": c.Impl = quick.New() case "merge": c.Impl = merge.New() case "bubble": panic(...) } return c } } 

导出功能使您可以在不知道结构的情况下定义包参数
程序配置以及如何获取其值。

github.com/gobwas/flagutil


我们找出了一个大的配置结构并设定了参数
独立的,但尚不清楚如何将它们融合在一起并获得
价值观。


正是为了解决这个问题,编写了flagutil软件包。


将参数放在一起


程序,其程序包和第三方库的所有参数均接收其前缀
并在包级别main处收集:


 package main import ( "flag" "app/yoogle" "app/yooglecfg" "github.com/gobwas/flagutil" ) func main() { flags := flag.NewFlagSet("my-app", flag.ExitOnError) var port int flag.IntVar(&port, "port", 4050, "port to bind to", ) var config *yoogle.Config flagutil.Subset(flags, "yoogle", func(sub *flag.FlagSet) { config = yooglecfg.Export(sub) }) } 

flagutil.Subset()函数做一件简单的事情:它添加一个前缀
"yoogle" )到回调内部sub注册的所有参数。


现在运行程序可能如下所示:


 app -port 4050 -yoogle.endpoint https://example.com -yoogle.timeout 10s 

获取参数值


flag.FlagSet中的所有参数flag.FlagSet包含flag.FlagSet的实现,
它具有Set(string) error方法-也就是说,它提供了机会
设置字符串表示形式


它仍然可以以键值对的形式从任何来源读取值,并且
进行flag.Set(key, value)调用。


这使我们有机会甚至不使用命令参数的语法
flag包中描述的行。 您可以通过任何方式解析参数,
例如posix程序参数

 package main func main() { flags := flag.NewFlagSet("my-app", flag.ExitOnError) // ... flags.String( "config", "/etc/app/config.json", "path to configuration file", ) flagutil.Parse(flags, // First, use posix arguments syntax instead of `flag`. // Just to illustrate that it is possible. flagutil.WithParser(&pargs.Parser{ Args: os.Args[1:], }), // Then lookup for "config" flag value and try to // parse its value as a json configuration file. flagutil.WithParser(&file.Parser{ PathFlag: "config", Syntax: &json.Syntax{}, }), ) } 

因此, config.json文件可能如下所示:


 { "port": 4050, "yoogle": { "endpoint": "https://example.com", "timeout": "10s" ... } } 

结论


当然,我不是第一个谈到这种方法的人。 许多
上述想法在几年前已经被一种或另一种方式使用,当时
我在MailRu工作。


因此,为了简化我们的应用程序的配置而不浪费时间
研究(甚至编写)下一个配置框架
以下内容:


  • 使用flag作为参数定义界面
    该程序
  • 分别导出每个软件包的参数,而无需了解结构和
    一种稍后获得价值的方法
  • main定义如何读取值,前缀和配置结构

flagutil库的flagutil我对库的认识
Peterbourgon / ff-我不会写flagutil ,如果不是这样的话
使用中的差异。


感谢您的关注!


参考文献


Source: https://habr.com/ru/post/zh-CN479882/


All Articles