跳到主要内容

07-配置

设计理念

1.支持多种配置源

Kratos定义了Source和Watcher接口来适配各种配置源。

package config

// 所有配置中心只有实现了【Source】接口,才能被集成到 kratos 框架中。
// KeyValue is config key value.
type KeyValue struct {
Key string
Value []byte
Format string
}

// Source is config source. 必须实现
type Source interface {
Load() ([]*KeyValue, error)
Watch() (Watcher, error)
}

// Watcher watches a source for changes. 可以不实现
type Watcher interface {
Next() ([]*KeyValue, error)
Stop() error
}

框架内置了本地文件file的实现:

var _ config.Source = (*file)(nil)

type file struct {
path string
}

// NewSource new a file source.
func NewSource(path string) config.Source {
return &file{path: path}
}

func (f *file) loadFile(path string) (*config.KeyValue, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return nil, err
}
info, err := file.Stat()
if err != nil {
return nil, err
}
return &config.KeyValue{
Key: info.Name(),
Format: format(info.Name()),
Value: data,
}, nil
}

func (f *file) loadDir(path string) (kvs []*config.KeyValue, err error) {
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
for _, file := range files {
// ignore hidden files
if file.IsDir() || strings.HasPrefix(file.Name(), ".") {
continue
}
kv, err := f.loadFile(filepath.Join(path, file.Name()))
if err != nil {
return nil, err
}
kvs = append(kvs, kv)
}
return
}

func (f *file) Load() (kvs []*config.KeyValue, err error) {
fi, err := os.Stat(f.path)
if err != nil {
return nil, err
}
if fi.IsDir() {
return f.loadDir(f.path)
}
kv, err := f.loadFile(f.path)
if err != nil {
return nil, err
}
return []*config.KeyValue{kv}, nil
}

func (f *file) Watch() (config.Watcher, error) {
return newWatcher(f)
}

另外,在contrib/config下面,我们也提供了nacos,apollo,etcd的配置中心的适配供使用:

2.支持多种配置格式

配置组件复用了encoding中的反序列化逻辑作为配置解析使用。默认支持以下格式的解析:

  • json
  • proto
  • xml
  • yaml

框架将根据配置文件类型匹配对应的Codec,进行配置文件的解析。您也可以通过实现Codec并用encoding.RegisterCodec方法,将它注册进去,来解析其它格式的配置文件。

配置文件类型的提取,根据配置源具体实现不同而略有区别,内置的file是把文件后缀作为文件类型的,其它配置源插件的具体逻辑请参考对应的文档。

3.配置合并

在config组件中,所有的配置源中的配置(文件)将被逐个读出,分别解析成map,并合并到一个map中去。因此在加载完毕后,不需要再理会配置的文件名,不用文件名来进行查找,而是用内容中的结构来对配置的值进行索引即可。设计和编写配置文件时,请注意各个配置文件中,根层级的key不要重复,否则可能会被覆盖

举例:

有如下两个配置文件:

# 文件1
foo:
baz: "2"
biu: "example"
hello:
a: b
# 文件2
foo:
bar: 3
baz: aaaa
hey:
good: bad
qux: quux

.Load后,将被合并为如下的结构:

{
"foo": {
"baz": "aaaa",
"bar": 3,
"biu": "example"
},
"hey": {
"good": "bad",
"qux": "quux"
},
"hello": {
"a": "b"
}
}

我们可以发现,配置文件的各层级将分别合并,在key冲突时会发生覆盖,而具体的覆盖顺序,会由配置源实现中的读取顺序决定,因此这里重新提醒一下,各个配置文件中,根层级的key不要重复,也不要依赖这个覆盖的特性,从根本上避免不同配置文件的内容互相覆盖造成问题。

在使用时,可以用.Value("foo.bar")直接获取某个字段的值,也可以用.Scan方法来将整个map读进某个结构体中,具体使用方式请看下文。

使用

1.初始化配置源

使用file,即从本地文件加载: 这里的path就是配置文件的路径,这里也可以填写一个目录名,这样会将整个目录中的所有文件进行解析加载,合并到同一个map中。

import (
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/config/file"
)

path := "configs/config.yaml"
c := config.New(
config.WithSource(
file.NewSource(path),
)

如果想用外部的配置中心,可以在contrib/config里面找一个,以consul为例:

import (
"github.com/go-kratos/kratos/contrib/config/consul/v2"
"github.com/hashicorp/consul/api"
)

consulClient, err := api.NewClient(&api.Config{
Address: "127.0.0.1:8500",
})
if err != nil {
panic(err)
}
cs, err := consul.New(consulClient, consul.WithPath("app/cart/configs/"))
if err != nil {
panic(err)
}
c := config.New(config.WithSource(cs))

不同的配置源插件使用方式略有差别,您可以参考它们各自的文档或examples。

2.读取配置

首先要定义一个结构体用来解析字段,如果您使用的是kratos-layout创建的项目,可以参考后面讲解kratos-layout的部分,使用proto文件定义配置和生成struct。

我们这里演示的是手工定义结构,您需要在结构体上用json tag来定义您配置文件的字段。

var v struct {
Service struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"service"`
}

使用之前创建好的config实例,调用.Scan方法,读取配置文件的内容到结构体中,这种方式适用于完整获取整个配置文件的内容。

// Unmarshal the config to struct
if err := c.Scan(&v); err != nil {
panic(err)
}
fmt.Printf("config: %+v", v)

使用config实例的.Value方法,可以单独获取某个字段的内容。

name, err := c.Value("service.name").String()
if err != nil {
panic(err)
}
fmt.Printf("service: %s", name)

3.监听配置变更

通过.Watch方法,可以监听配置中某个字段的变更,在本地或远端的配置中心有配置文件变更时,执行回调函数进行自定义的处理

if err := c.Watch("service.name", func(key string, value config.Value) {
fmt.Printf("config changed: %s = %v\n", key, value)
// 在这里写回调的逻辑
}); err != nil {
log.Error(err)
}

4.配置解析Decoder

Decoder用于将配置文件内容用特定的反序列化方法解析出来,默认decoder会根据文件的类型自动识别类型并解析,通常情况不需要自定义这个,您可以通过后文的实现Codec的方式来注册更多文件类型。

在初始化config时加入WithDecoder参数,可以将Decoder覆盖为自定义的逻辑。如下代码展示了配置自定义Decoder的方法,这里使用了yaml库解析所有配置文件,您可以使用这种方式来使用特定的配置文件解析方法,但更推荐使用后文的实现Codec的方式,能同时支持多种格式的解析。

import "gopkg.in/yaml.v2"

c := config.New(
config.WithSource(
file.NewSource(flagconf),
),
config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
return yaml.Unmarshal(kv.Value, v)
}),
)

使用

1.定义

修改internal/conf/config.proto文件的内容,在这里使用Protobuf IDL定义你配置文件的结构。您也可以在这个目录下创建新的proto文件来定义额外的配置格式。

2.生成

在项目根目录执行下面的命令即可生成用来解析配置文件的结构体:

make config

执行成功后,您应该能看到config.pb.go生成在config.proto文件的旁边,您就可以使用里面的结构体,比如Bootstrap来读取您的配置。