
大家好! 在Go上进行了五年编程之后,我发现自己相当
特定程序配置方法的热心支持者。 在这
我将尝试在文章中揭示他的主要思想,并分享一小部分
实现这些想法的图书馆。
显然,本文是非常主观的,并不假装是客观的
真相 但是,我希望它对社区有用,并有助于减少
在这样琐碎的任务上花费的时间。
你在说什么
一般来说,我认为配置是对我们变量的定义
我们可以在运行时从外部获取其值的程序。
它可以是参数或命令行参数,环境变量,
存储在磁盘上或网络上任何地方的配置文件,数据库表
数据等等。
由于Go是一种高度静态的类型化语言,因此我们希望
确定并获取此类变量的值,并考虑其类型。
有大量的开放源代码库甚至框架,
解决这类问题。 他们中的大多数代表了自己的愿景。
如何做到的。
我想谈谈一种不太常见的程序配置方法。
而且,在我看来,这种方法最简单。
flag
包
是的,这不是一个玩笑,我真的想引起您的注意
著名的Go标准库包。
乍一看, flag
是用于处理命令参数的工具
线,仅此而已。 但是此包也可以用作接口。
确定我们程序的参数。 在讨论的方法中
flag
主要用于这种方式。
如上所述,我们希望输入类型的参数。
flag
包提供了对大多数基本类型执行此操作的能力。
flag.String()
, flag.Int()
甚至是flag.Duration()
。
对于更复杂的类型,例如[]string
或time.Time
有一个接口
flag.Value
,它允许您描述flag.Value
从中获取参数的值
字符串表示 。
例如,类型为time.Time
的参数可以这样实现:
软件包的重要属性是它在标准库中的存在- 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
。
可以在yoogle
包yoogle
或包中声明此类函数
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 } }
导出功能使您可以在不知道结构的情况下定义包参数
程序配置以及如何获取其值。
我们找出了一个大的配置结构并设定了参数
独立的,但尚不清楚如何将它们融合在一起并获得
价值观。
正是为了解决这个问题,编写了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)
因此, config.json
文件可能如下所示:
{ "port": 4050, "yoogle": { "endpoint": "https://example.com", "timeout": "10s" ... } }
结论
当然,我不是第一个谈到这种方法的人。 许多
上述想法在几年前已经被一种或另一种方式使用,当时
我在MailRu工作。
因此,为了简化我们的应用程序的配置而不浪费时间
研究(甚至编写)下一个配置框架
以下内容:
- 使用
flag
作为参数定义界面
该程序 - 分别导出每个软件包的参数,而无需了解结构和
一种稍后获得价值的方法 - 在
main
定义如何读取值,前缀和配置结构
flagutil
库的flagutil
我对库的认识
Peterbourgon / ff-我不会写flagutil
,如果不是这样的话
使用中的差异。
感谢您的关注!
参考文献