通常情况下,我们的应用程序会涉及到很多配置,比如 HTTP/RPC 监听端口、日志相关配置、数据库配置等。这些配置可以来自不同的配置源,例如本地配置文件、环境变量、命令行参数,或是 Consul 一类的远程配置中心,同时一项配置可能在多个不同的配置源中同时存在,需要进行优先级处理。这篇文章介绍使用 koanf 在 Golang 程序中进行配置管理。
viper 是 Golang 程序中被广泛使用的配置解析库,是一个开箱即用的配置 SKD,但是在使用过程中,viper 也暴露出来一些问题:
viper 的代码库实现很复杂,短时间内很难了解它的设计思路。viper 对外没有暴露可扩展的语义,在实际使用过程中,如果遇到无法覆盖的应用场景,往往需要在业务层进行单独处理,会对业务逻辑增加额外的侵入性。因此需要一个更加轻量级,且易于扩展的配置管理实现。
概述
knanf 是一个轻量且易于扩展定制的 Golang 配置库,v2 版本通过独立的 Golang Module,将外部依赖项都与核心分离,并且可以根据需要单独安装,使得其核心代码只有一千余行:
-
JSON、Yaml 等数据格式的解析扩展,只需要实现Parser
接口,独立于 Parser 目录中:koanf/parsers;
1
2
3
4
|
type Parser interface {
Unmarshal([]byte) (map[string]interface{}, error)
Marshal(map[string]interface{}) ([]byte, error)
}
|
-
Consul、File、Env 等配置源的数据解析,只需要实现Provider
接口,独立 Provider 目录中:koanf/providers;
1
2
3
4
|
type Provider interface {
ReadBytes() ([]byte, error)
Read() (map[string]interface{}, error)
}
|
-
koanf 提供了 Provider 与 Parser 接口,通过实现对应的接口,可以接入更多的配置解析方式;
-
koanf 自身并未规定配置源的优先级,koanf 会按照 Provider 的调用顺序,覆盖已有的值;
koanf 的基础操作可以参考 knanf/readme,文中将不再赘述,以下内容将会在了解使用方式的基础上进行介绍。
配置优先级
koanf 会按照 Provider 的调用顺序,覆盖已有的值。因此我们可以通过封装统一的配置解析步骤,达到默认优先级的效果。例如下面的代码实现了一个 Parse()
函数,其参数有配置路径与数据格式,以及对应的 Parser(在示例中,支持本地文件与 Consul):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
func Parse(configPath, format string, reader ParserFunc, opts ...OptionFunc) error {
var parser koanf.Parser
switch format {
case ConfigFormatYAML:
parser = kyaml.Parser()
case ConfigFormatJSON:
parser = kjson.Parser()
default:
return fmt.Errorf("unsupported config format: %s", format)
}
if err := reader(configPath, parser); err != nil {
return err
}
}
// OptionFunc is the option function for config.
type OptionFunc func(map[string]any)
// ParserFunc Parse config option func
type ParserFunc func(path string, parser koanf.Parser) error
func ReadFromFile(filePath string, parser koanf.Parser) error {
if err := k.Load(kfile.Provider(filePath), parser); err != nil {
// Config file was found but another error was produced
return fmt.Errorf("error loading config from file [%s]: %w", filePath, err)
}
return nil
}
// ReadFromConsul read config from consul with format
func ReadFromConsul(configPath string, parser koanf.Parser) error {
if err := k.Load(kconsul.Provider(kconsul.Config{}), parser); err != nil {
return fmt.Errorf("error loading config from consul: %w", err)
}
return nil
}
|
配置数据可以以不同的数据格式与存储方式进行组织,得益于 koanf 优秀的解耦思想,我们可以很容易的支持内部的配置中心
在容器化部署的情况下,我们还会根据环境变量来修改当前程序的某些配置项,以便于在不修改镜像内容的情况下,进行动态配置。ENV Provider 通过统一前缀来标识所需要解析的环境变量,并提供了一个可选的 callback 函数,让用户自定义环境变量名称的处理逻辑。在示例代码中,我们自定义的回调函数去除了统一前缀,并将所有大写字符转为小写:
1
2
3
4
5
6
7
8
9
10
11
12
|
// Second, read config from environment variables,
// Parse environment variables and merge into the loaded config.
// "PUDDING" is the prefix to filter the env vars by.
// "." is the delimiter used to represent the key hierarchy in env vars
// The (optional, or can be nil) function can be used to transform
// the env var names, for instance, to lowercase them
if err := k.Load(kenv.Provider("PUDDING/", defaultDelim, func(s string) string {
return strings.ReplaceAll(strings.ToLower(
strings.TrimPrefix(s, "PUDDING/")), "/", ".")
}), nil); err != nil {
return fmt.Errorf("error loading config from env: %w", err)
}
|
CLI 优先级
CLI 优先级是一个特殊的场景,他具有默认值,但是可以被命令行参数覆盖。由于 koanf repo 内并未实现 Golang 标准库 flag 的配置解析。因此需要自己实现一个 Provider。
为了便于理解,以下代码段仅贴出了关键部分。Golang flagSet 提供了VisitAll
方法,以便于调用方能够访问 flagSet 中所有被命令行参数设置与未被设置的 flag。同时标准库中的所有 flag Value 类型都实现了flag.Getter
方法,能够获取该 flag 的当前值,以及该值的字符串类型表示方法。我们将当前值与初始值进行比对,就能感知到命令行参数是否被设置。同时 koanf 也实现了 Exist 方法,用来判断 key 是否已经存在:
- 如果命令行参数没有设置,并且低优先级的配置文件也没有设置,那么就使用 CLI 默认值;
- 如果命令行参数没有设置,但是低优先级的配置文件设置了,那么就使用低优先级的配置文件的值;
- 如果命令行参数设置了,那么就使用命令行参数的值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// Read reads the flag variables and returns a nested conf map.
func (p *Flag) Read() (map[string]any, error) {
mp := make(map[string]any)
p.flagSet.VisitAll(func(f *flag.Flag) {
var (
key string
value any
)
if p.cb != nil {
key, value = p.cb(f.Name, f.Value)
} else {
// All Value types provided by flag package satisfy the Getter interface
// if user defined types are used, they must satisfy the Getter interface
getter, ok := f.Value.(flag.Getter)
if !ok {
panic(fmt.Sprintf("flag %s does not implement flag.Getter", f.Name))
}
key, value = f.Name, getter.Get()
}
// if the key is set, and the flag value is the default value, skip it
if p.ko.Exists(key) && f.Value.String() == f.DefValue {
return
}
mp[key] = value
})
return maps.Unflatten(mp, p.delim), nil
}
|
最后Read
方法的返回值是一个map[string]any
哈希表,koanf 会将该哈希表与已有的配置进行合并覆盖。
配置 Watch
配置的Watch
方法并不包含在 koanf 的核心模块中,而是由 Provider 独自实现。我们需要向 Provider 注册一个回调函数,当有配置变更事件到来时,加载新的配置信息,并让业务模块感知到配置的变化。
1
2
3
4
5
6
7
8
|
f := file.Provider("mock/mock.json")
if err := k.Load(f, json.Parser()); err != nil {}
f.Watch(func(event interface{}, err error) {
k.Load(f, json.Parser())
k.Print()
// trigger reload config
})
|
配置反序列化
koanf 所有的配置都会被保存在一个全局的 map 中,但是 map 是一个无类型的容器,是一个宽松的数据结构,不利于做类型检查与数据校验。
在实际使用过程中,我们往往需要将配置反序列化为结构体,以便于在代码中使用。koanf 提供了Unmarshal
方法,可以将配置反序列化为结构体,因此我们需要在结构体中添加mapstructure
标签,以便于 koanf 反序列化时能够正确的解析字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type BaseConfig struct {
HostDomain string `json:"host_domain" yaml:"host_domain" mapstructure:"host_domain"`
GRPCPort int `json:"grpc_port" yaml:"grpc_port" mapstructure:"grpc_port"`
HTTPPort int `json:"http_port" yaml:"http_port" mapstructure:"http_port"`
EnableTLS bool `json:"enable_tls" yaml:"enable_tls" mapstructure:"enable_tls"`
}
func UnmarshalToStruct(path string, c any) error {
if err := k.UnmarshalWithConf(path, c, koanf.UnmarshalConf{Tag: "mapstructure"}); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err)
}
return nil
}
|
总结
koanf 最精髓的设计之处在于,将 Parser、Provider 与 Core 解耦,通过插件的形式引入,每一个模块都可以根据需要单独安装。并且自定义实现 Provider 的成本低廉,能够便捷地进行功能扩展。