在golang应用中优雅的使用配置文件,并且简单优雅的快速接入分布式配置中心

配置文件类型有很多种,像常用的properties、yaml、ini、xml、json等,还有不太常见的plist(xml)、toml、 HOCON等,还有很多特别自定义的格式,甚至有些就直接使用脚本语言来替代,像python、js,groovy等,无论是哪种配置文件,基本上都是三种类型:

  • 结构化:yaml,xml,json

  • 非结构化扁平格式:properties

  • 半结构化和扁平结合

那么多的配置文件,如何选择呢?可以尝试给配置文件做个排序,来指导选择;对于常用的配置文件格式中,按照人类理解和方便编写配置来排序:

ini > properties > json > xml > yaml > toml

如果按照能配置复杂数据的复杂度来排序:

xml > yaml > toml/json/plist > ini > properties

对于这么多格式的配置文件如何选择呢?如果又或者应用了一个三方库,使用了和自己应用不一样的格式的配置文件,就不能统一了,不仅要引用各种格式解析的文件和获取配置项的三方库,而且各个三方库api又不一样,在程序中使用这些api,既混乱又得不到统一,有些库api支持的特性,另一种lib又不支持。

那么出现这种情况时,如何优雅的解决这个问题呢?这里今天的主角就要亮场了:props

props简介

props是一个统一的配置工具库,将各种配置源抽象或转换为类似properties格式的key/value,并提供统一的API来访问这些key/value。支持 properties 文件、ini 文件、zookeeper k/v、zookeeper k/props、consul k/v、consul k/props等配置源,并且支持通过 Unmarshal从配置中抽出struct;支持上下文环境变量的eval,${}形式;支持多种配置源组合使用。主要特性如下:

支持的配置源:

  • properties格式文件
  • ini格式文件
  • yaml格式文件
  • Apollok/v,k/props,k/ini,k/yaml
  • Nacosk/props[properties],k/yaml,k/ini,k/ini_props
  • zookeeper k/v
  • zookeeper k/props[properties],k/yaml,k/ini,k/ini_props
  • consul k/v
  • consul k/props[properties],k/yaml,k/ini,k/ini_props
  • etcd API V2 k/v
  • etcd API V2 k/props
  • etcd API V3 k/v
  • etcd API V3 k/props

key/value支持的数据类型:

  • key只支持string
  • value 5种数据类型的支持:
    • string
    • int
    • float64
    • bool
    • time.Time: 支持常见的格式和毫秒数
    • time.Duration:
      • 比如 “300ms”, “-1.5h” or “2h45m”.
      • 合法的时间单位: “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”.

其他特性(非常有用的特性)

  • Unmarshal支持, 可以将配置项注入到结构体中
  • 上下文变量eval支持,${}形式
  • 支持多配置源组合,包括本地和分布式中心的自由组合
  • 默认添加了系统环境变量,优先级最低

props思路

props通过其名称就可以知道,基于properties格式的配置项作为基础,将各种配置源抽象或转换为类似properties格式的key/value,其思路来源于spring的PropertySource和PropertySources。无论哪种配置格式的文件,在读取后都已key/value形式存储在map中,需要读取配置项的地方可以定义的配置key获取配置值。因此不同的配资源,不管是本地文件还是远程配置中心,只要将读取的配置内容转换为key/value即可,那么对于props不支持的配置文件或者格式也可以轻松扩展。由于使用key/value作为基础配置,缺点就是对复杂数据格式支持不够,但换个角度,对于大部分应用来说无需太复杂的配置格式,太复杂了容易出错,另外确实要用到,golang api本身就内置了xml和json的解析库,支持Unmarshal,很容易使用。

在这个key/value基本的基础之上,拓展了Unmarshal、变量引用、配置组合、系统环境变量等功能,更方便的来维护配置。

在props中统一将配置文件或者分布式配置中心统一抽象为配置源,任何本地或者通过网络能读取到的文本配置并且可以转换为key/value的配置载体,都可以作为props的配置源,扩展也很简单,只要实现kvs.ConfigSource接口即可。

总之,就是围绕优雅、简洁、易用、可靠几个特点来构建,花里胡哨的功能也无需多加。

下面就来看看如何使用props。

安装:

go get -u github.com/tietang/props/v3

先睹为快:

例子1:

main.go

 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
package main

import (
	"fmt"
	"github.com/tietang/props/v3/ini"
	"github.com/tietang/props/v3/kvs"
	"time"
)

func main() {

	conf := ini.NewIniFileConfigSource("config.ini")
	//
	fmt.Println("selection1:\n")
	fmt.Println(conf.GetInt("selection1.num"))
	fmt.Println(conf.GetDuration("selection1.duration0"))
	fmt.Println(conf.GetDurationDefault("selection1.duration1", 9*time.Second).Milliseconds())
	//
	fmt.Println("\nselection1.bool:\n")
	fmt.Println(conf.GetBool("selection1.bool.true0"))
	fmt.Println(conf.GetBool("selection1.bool.true1"))
	fmt.Println(conf.GetBool("selection1.bool.true2"))
	fmt.Println(conf.GetBool("selection1.bool.true3"))
	fmt.Println(conf.GetBool("selection1.bool.true4"))
	fmt.Println(conf.GetBool("selection1.bool.false0"))
	fmt.Println(conf.GetBool("selection1.bool.false1"))
	//
	fmt.Println("\nselection2.sub1:\n")
	fmt.Println(conf.GetTime("selection2.sub1.time0"))
	fmt.Println(conf.GetTime("selection2.sub1.time1"))
	fmt.Println(conf.GetTime("selection2.sub1.time2"))
	fmt.Println(conf.GetInt("selection2.sub1.int"))
	fmt.Println(conf.Get("selection2.sub1.string"))
	fmt.Println(conf.GetFloat64("selection2.sub1.float"))
	fmt.Println(conf.Ints("selection2.sub1.ints"))

}

配置文件config.ini

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[selection1]
num : 123
duration0 : 1s
duration1 : 92ms

[selection1.bool]
true0 : true
true1 : y
true2 : yes
true3 : on
true4 : 1
false0 : f
false1 : no

[selection2.sub1]
time0 : 2023-01-21 01:01:00
time1 : 1665317881
time2 : 2006-01-02 15:04:05 +0800
int : 123
string : 我是字符串
float : 12.34
ints : 1,2,3,4,5,6

运行结果如下:

 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
selection1:

123 <nil>
1s <nil>
92

selection1.bool:

true <nil>
true <nil>
true <nil>
true <nil>
true <nil>
false <nil>
false <nil>

selection2.sub1:

2023-01-21 01 : 01:00 +0000 UTC <nil>
2022-10-09 20 : 18:01 +0800 CST <nil>
2006-01-02 15 : 04:05 +0800 CST <nil>
123 <nil>
我是字符串 <nil>
12.34 <nil>
[1 2 3 4 5 6]

从例子中可以看到,只需要一行代码就可以加载配置:

1
conf := ini.NewIniFileConfigSource("config.ini") 

通过kvs.ConfigSource提供的api进行配置项读取:

  • Get(key string) (string, error)
  • GetDefault(key, defaultValue string) string
  • GetInt(key string) (int, error)
  • GetIntDefault(key string, defaultValue int) int
  • GetDuration(key string) (time.Duration, error)
  • GetDurationDefault(key string, defaultValue time.Duration) time.Duration
  • GetTime(key string) (time.Time, error)
  • GetTimeDefault(key string, defaultValue time.Time) time.Time
  • GetBool(key string) (bool, error)
  • GetBoolDefault(key string, defaultValue bool) bool
  • GetFloat64(key string) (float64, error)
  • GetFloat64Default(key string, defaultValue float64) float64

要读取数组:

  • Strings(key string) []string
  • Ints(key string) []int
  • Float64s(key string) []float64
  • Durations(key string) []time.Duration

要配置数组,可以用:”|“,”,“,” “(空格),进行分割。

组合多个配置源(建议使用):

props内置了配置源组合:kvs.CompositeConfigSource, 可以组合任意多个任意形式的kvs.ConfigSource配置源。

比如,多个Properties文件组合:

NewPropertiesCompositeConfigSource(fileNames …string)

组合多个kvs.ConfigSource:

NewDefaultCompositeConfigSource(configSources …ConfigSource)

在已有的 kvs.CompositeConfigSource追加kvs.ConfigSource配置源:

Add(css …ConfigSource)

AddAll(css []ConfigSource)

更多功能查看:https://github.com/tietang/props

props使用多种配置格式文件和分布式配置中心

在props简介中已经知道,props支持的配置源,使用不同的配置源,只需要使用对应的包名+New函数进行构建即可。

properties格式文件

  • 空内存Properties:kvs.NewMapProperties()
  • 单个Properties文件:kvs.NewPropertiesConfigSource(“config.props”)
  • 多个Properties文件:kvs.NewPropertiesCompositeConfigSource(files…)

ini格式文件

  • 单个ini文件:ini.NewIniFileConfigSource()
  • 多个ini文件:ini.NewIniFileCompositeConfigSource()

yaml格式文件

  • 通过yaml文本:yam.ByYaml()
  • 单个yaml文件:yam.NewYamlConfigSource()
  • 多个yaml文件:yam.NewYamlFileCompositeConfigSource()

其他配置中心载体(zk,consul,etcd)

以下都类同,具体查看godoc或源码例子:

  • zookeeper k/v
  • zookeeper k/props[properties],k/yaml,k/ini,k/ini_props
  • consul k/v
  • consul k/props[properties],k/yaml,k/ini,k/ini_props
  • etcd API V2 k/v
  • etcd API V2 k/props
  • etcd API V3 k/v
  • etcd API V3 k/props

使用apollo作为配置源:

Apollo:支持多种格式k/v,k/props,k/ini,k/yaml

  • 多namespace:apollo.NewApolloConfigSource()
  • 多namespace+系统环境变量:apollo.NewApolloCompositeConfigSource()

只需要执行configService地址、appId,多个namespace即可构建一个kvs.ConfigSource配置源

1
2
3
4
5
6
7
8
conf := apollo.NewApolloConfigSource("81.68.181.139:8080", "SampleApp", []string{
"application", "mxf",
})
keys := conf.Keys()
for _, key := range keys {
value := conf.GetDefault(key, "null")
fmt.Println(key, "=", value)
}

如果configService暴露在公网且配置了应用访问密钥,可以使用WithSecret来指定访问密钥:

1
2
3
conf := apollo.NewApolloConfigSourceWithSecret("81.68.181.139:8080", "SampleApp", "ecd6939c4d0d4ac0be3cb2ca81eba3db", []string{
"application", "mxf",
})

默认会监听该conf配置的所有Namespace, 如果不想监听某个namespace的配置更新,可以使用remove方法移除:

1
RemoveWatchedNamespace(namespace)

使用nacos作为配置源:

[Nacos:支持多种格式 k/props[properties],k/yaml,k/ini,k/ini_props

  • 单个dataid:nacos.NewNacosClientConfigSource()
  • 多dataid:nacos.NewNacosClientCompositeConfigSource()
  • 单个dataid和kv格式:nacos.NewNacosClientPropsConfigSource()
  • 多dataid和kv格式:nacos.NewNacosClientPropsCompositeConfigSource()

指定nacos服务地址、命名空间、1个或多个dataId,多个dataId会合并

1
2
3
4
5
6
    address := "10.99.71.54:8848"
namespaceId := "dzpl"
dataId := "monitoring-collector"
group := "dev"
conf := nacos.NewNacosClientConfigSource(address, group, namespaceId, dataId)
fmt.Println(conf.Get("apm.thrift.port"))

如果一个应用有多个dataId,可以使用 NewNacosClientCompositeConfigSourcel 来构建:

1
conf := nacos.NewNacosClientCompositeConfigSource(address, group, namespaceId, dataIds...)

默认会自动监听配置的dataId的配置更新,如果不希望某个dataIdId的配置不被热更新,可以调用cancel方法:

NacosClientConfigSource.CancelListening()

关于配置格式定义说明

配置格式支持:properties/props、yaml/yml/yam、ini 3种格式。

不同的配置中心对于配置格式的配置方法和获取不一样,apollo的配置格式可以通过namespace来获取,apollo namespace命名上默认是Properties格式,非Properties格式都要带上格式扩展名称,因此在props中通过apollo namespace扩展名来区分配置格式,默认为Properties,行为和apollo保持一致。对于nacos,虽然nacos控制台UI中支持text、json、xml、yaml、html、properties几种格式的编辑,但是由于nacos 配置获取的api中无法区分配置格式,仅仅在控制台编辑和展示时用到。另外,像ini格式的配置apollo和nacos都不支持,所以需要用一种方法在配置中标识所配置的内容是什么格式。其他配置中心也有类似的问题。

下面就目前比较流行的2中配置中心apollo和nacos进行配置格式标识的设计,通过2种方法来标识:

  • 在关键名称上来标识,nacos中的dataId和apollo中的namespace都可以来标识
  • 在配置内容文本中通过注释来标识

在关键名称上来标识

将格式类型作为后缀追加在关键名称上,nacos 追加在dataId上,apollo可以追加在namespace,一名称的后缀形式来标识,使用. , -, _ 任意一个分割符分割即可,比如:

命名为:

  • test_ini
  • test-ini
  • test.ini

都可以,那么:

apollo中对应完整namespace名称为:

  • test_ini.txt
  • test-ini.txt
  • test.ini.txt

naco中对应完整dataId名称为:

  • test_ini
  • test-ini
  • test.ini

在配置内容文本中通过注释来标识

在配置文件首行通过注释的形式来标识配置格式:

#@, ;@, //@, @,并定义配置内容格式的信息,

比如:;@ini , #@yaml, #@yml等.

支持的格式标识有:;@ini,#@yaml, #@yml, #@yam,#@props,#@properties,

对于yaml和Properties格式,在界面上选择对应的格式或者text即可,对于ini格式选择text,并加标识;@ini即可。

需要注意不同格式的注释符号。

ini格式的标识:

apollo和nacos都不支持ini格式的配置,apollo中有txt,nacos中有text,2种标识方法都可以使用,选择配置格式是选择txt或text即可。

多配置的配置加载的优先级

  • 后加载优先逻辑,也就是说后面加载的覆盖先加载的。注意后加载逻辑不等于配置顺序。

  • 仅仅适用于kvs.CompositeConfigSource

对于kvs.CompositeConfigSource ,加载优先级顺序和配置顺序相同,先配置的优先级最高,后配置的优先级最低,也就是说通过NewDefaultCompositeConfigSource和NewCompositeConfigSource创建实例时,以及Add方法增加kvs.ConfigSource时,优先级是从左往右,从前往后。比如下面的代码中:

1
2
3
4
    var conf0 kvs.ConfigSource
var conf1 kvs.ConfigSource
var conf2 kvs.ConfigSource
conf := kvs.NewDefaultCompositeConfigSource(conf0, conf1, conf2)

conf0优先级最高,conf2优先级最低,优先级依次为:conf0>conf1>conf2。

如果3个conf中存在同样的key,则以conf0中的为准;如果conf1和conf2中存在同样的key,则以conf1中的为准。

上下文变量引用${x.y.key}

在配置中使用变量引用:${x.y.key}。

props是支持配置上下文变量引用,在引用上下文中不区分优先级,仅仅依赖于配置加载的优先级。

目前仅仅kvs.CompositeConfigSource支持变量引用,建议使用New***CompositeConfigSource函数进行构建kvs.ConfigSource。

配置变量引用通过${x.y.key}形式来引用。

下面是一个变量引用的例子:

config.ini

1
2
3
4
5
6
7
[app]
name = appName
port = 8080

[log]
;值为appName
file.name = ${app.name}

也支持多个变量组合,在字符串中嵌入变量引用即可:

1
2
3
4
5
6
7
8
[app]
name = appName
port = 8080

[log]
file.name = ${app.name}
;值为./logs/appName-8080/ 
dir = ./logs/${app.name}-${app.port}/

Unmarshal为自定义结构体:

props支持将配置Unmarshal一个结构体实例。

先看看下面的例子

定义结构体:

1
2
3
4
type App struct {
Name string
Port int
}

配置文件:

1
2
3
[app]
name = appName
port = 8080

Unmarshal:

1
2
3
4
5
6
conf := ini.NewIniFileCompositeConfigSource(file)
app := new(App)
err := conf.Unmarshal(app, "app")
//或者:kvs.Unmarshal(conf,app,"app")
fmt.Println(err)
fmt.Println(app)

使用时需要注意:

  1. 需要指定一个配置前缀,前缀在Unmarshal时会作为key的匹配和过滤中要使用到。
  2. 过滤掉key前缀后,剩余的key需要和结构体字段名称对应,匹配2种对应格式:
    • 小写字母开头的驼峰命名方式,比如:Name>name,LogName > logName
    • 中划线分割的全小写命名方式, 比如:LogName > log-name

支持嵌套结构体,对应key使用.分割

比如配置文件如下:

1
2
3
4
5
6
7
8
[app]
name = appName
port = 8080

[app.log]
fileName = ${app.name}
;或者file-name = ${app.name}
dir = ./logs/${app.name}-${app.port}/

对应于结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type App struct {
Name string
Port int
Log  Log
}

type Log struct {
FileName string
Dir      string
}

使用系统环境变量

kvs.CompositeConfigSource构建默认都添加了系统环境变量,可以通过kvs.ConfigSource来读取系统环境变量。

如果不需要系统环境变量,可以通过:kvs.NewEmptyNoSystemEnvCompositeConfigSource()进行构建实例。

下面的例子可以查看加载了那些系统环境变量:

1
2
conf := kvs.NewEmptyCompositeConfigSource()
fmt.Println(conf.Keys())

更多用法和特性查看:https://github.com/tietang/props